九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、前回「Vite + Inertia + React」でログイン機能をつくってみましたが、今回もその流れで基本的な「追加・変更・削除・閲覧」部分(いわゆる CRUD
)をつくってみることにしました。
なお、できるだけコンパクトにしたかったのですが、気がつけば1万文字を大きく超えるボリュームになってしまい「2回に分割した方がよかったのでは…」と思ったりもしました。
しかし「技術系ブログ界のこち亀」のような、いつ読み始めても楽しめる(できるだけ続きものにはしない)スタンスでブログ運営をしていることもあり、あえて1回で公開することにしました(笑)
そこで❗
今回は「Laravel + Vite + Inertia + React」で「追加・変更・削除・閲覧」機能をつくってみます。
ぜひ何かの参考になりましたら嬉しいです😊✨
「人生で初めて
ル・プティ・ムートンという
高級ワインを飲みました🍷✨
(ただし、自分では買ってないです)」
開発環境: Laravel 9.x、Vite、Inertia、ReactJS
目次
必要なファイルをつくる
では、まずはLaravel
側で必要なファイルをつくります。
以下のコマンドで一気に「モデル」「マイグレーション」「Seeder(テストデータ作成)」「コントローラー」を用意しましょう。
php artisan make:model Post -msc
では、それぞれ作成されたファイルの中身を変更していきます。
マイグレーション
まずはDB
の設計図になるマイグレーションです。
database/migrations/****_**_**_******_create_posts_table.php
// 省略 public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title')->comment('タイトル'); $table->text('body')->comment('本文'); $table->timestamps(); }); } // 省略
Seeder
次に、DB
にテストデータを用意できるSeeder
ファイルです。
database/seeders/PostSeeder.php
// 省略 class PostSeeder extends Seeder { // 省略 public function run() { for($i = 0; $i < 25; $i++) { Post::create([ 'title' => 'タイトル' . $i, 'body' => '本文' . $i, ]); } } }
なお、Seeder
は作成しただけでは有効になりませんので、DatabaseSeeder.php
へ登録します。
database/seeders/DatabaseSeeder.php
// 省略 class DatabaseSeeder extends Seeder { // 省略 public function run() { // 省略 $this->call(PostSeeder::class); // 👈 ここを追加しました } }
では、この状態でDBを初期化してみましょう。
以下のコマンドを実行してください。
php artisan migrate:fresh --seed
すると、posts
テーブルは次のようになります。
コントローラー
そして、コントローラーです。
app/Http/Controllers/PostController.php
<?php namespace App\Http\Controllers; use App\Http\Requests\PostRequest; use App\Models\Post; use Inertia\Inertia; use Illuminate\Http\Request; class PostController extends Controller { public function index() { $posts = Post::paginate(5); return Inertia::render('Post/Index', [ 'title' => 'Laravel: Vite + Inertia + React で CRUD サンプル', 'posts' => $posts, 'message' => session('message'), ]); } public function create() { return Inertia::render('Post/Create'); } public function store(PostRequest $request) { $post = new Post(); $post->title = $request->title; $post->body = $request->body; $post->save(); return redirect()->route('post.index')->with([ 'message' => '登録が完了しました', ]); } public function show(Post $post) { return Inertia::render('Post/Show', [ 'post' => $post, ]); } public function edit(Post $post) { return Inertia::render('Post/Edit', [ 'post' => $post, ]); } public function update(PostRequest $request, Post $post) { $post->title = $request->title; $post->body = $request->body; $post->save(); return redirect()->route('post.index')->with([ 'message' => '変更が完了しました', ]); } public function destroy(Request $request, Post $post) { $post->delete(); // ページ番号つきでリダイレクト(削除時にページ移動してしまわないため) return redirect() ->route('post.index', ['page' => $request->page]) ->with('message', '削除が完了しました'); } }
コードは少し長いものの、通常のCRUD
とほぼ同じです。
違う部分といえば、Inertia::render()
の部分ですね。
これは、Inertia
用のデータを用意するためのもので、後でつくることになるresources/js/Pages
フォルダ内のビュー(テンプレート)を呼び出しています。
バリデーションをつくる
先ほどのコントローラー内でセットしたPostRequest
が、まだ存在していないので、ここでつくっていきます。
以下のコマンドを実行してください。
php artisan make:request PostRequest
すると、ファイルが作成されるので、中身を以下のように変更します。
app/Http/Requests/PostRequest.php
// 省略 class PostRequest extends FormRequest { public function authorize() { return true; } public function rules() { return [ 'title' => 'required|string|max:255', 'body' => 'required|string|max:255', ]; } }
【追記:2023.03.12】訪問者さんからのご質問をいただいたので、コードを変更しました。
これで、PostController
内のstore()
とedit()
でバリデーション(入力チェック)が有効になりました。
ビューをつくる
では、実際にブラウザから見ることになるビュー(テンプレート)を作っていきましょう。
作成するページは以下の4つです。
- 一覧: index
- 個別表示: show
- データ追加: create
- データ変更: edit
では、順に見ていきましょう。
index
resources/js/Pages/Post/Index.jsx
import React from "react"; import { Inertia } from "@inertiajs/inertia"; import { Link } from '@inertiajs/inertia-react'; import Pagination from "@/Components/Pagination"; export default function Index(props) { // Data const { message } = props; // Methods const onDelete = id => { if(confirm('削除します。よろしいですか?')) { const url = route('post.destroy', id); const params = { _method: 'delete', page: props.posts.current_page }; // delete() ではパラメータを送信できないため、あえて post() を使っています Inertia.post(url, params); } }; return ( <div className="p-5"> <h1 className="font-bold">{props.title}</h1> {message && <div id="message" className="mt-2 text-green-700 bg-green-100 p-3 rounded-lg">{message}</div>} <div className="text-right p-3 mb-2"> <Link className="text-white bg-green-700 rounded-lg text-sm px-4 py-2 mr-2" href={route('post.create')}>+ 追加する</Link> </div> <table className="w-full bg-gray-100"> <thead className="bg-blue-100"> <tr> <th>ID</th> <th>タイトル</th> <th>操作</th> </tr> </thead> <tbody> {props.posts.data.map(post => ( <tr key={post.id}> <td className="p-2 border">{post.title}</td> <td className="p-2 border">{post.body}</td> <td className="p-2 border"> <Link className="text-white bg-gray-400 rounded-lg text-sm px-4 py-2 mr-2" href={route('post.show', { id: post.id })}> 確認 </Link> <Link className="text-white bg-blue-700 rounded-lg text-sm px-4 py-2 mr-2" href={route('post.edit', { id: post.id })}> 変更 </Link> <button className="text-white bg-red-700 rounded-lg text-sm px-4 py-2 mr-2" onClick={() => onDelete(post.id)}> 削除 </button> </td> </tr> ))} </tbody> </table> <Pagination data={props.posts} /> </div> ); }
ループしているところなどVue
と似た部分もあるので、Vue
に慣れている方なら比較的シンプルに完了できるかと思います。(ちなみに、route()
が使えるのは便利ですね👍)
なお、<Pagination />
の部分は独自コンポーネントになりますので、後でつくることにします。
show
resources/js/Pages/Post/Show.jsx
import React from "react"; import { Link, Head } from '@inertiajs/inertia-react'; export default function Show(props) { const { title, body } = props.post; return ( <div className="p-4"> <h1 className="font-bold mb-3">{title}</h1> <div className="mb-3">{body}</div> <Link className="text-white bg-blue-700 rounded-lg text-sm px-4 py-2" href={route('post.index')}>戻る</Link> </div> ); }
post
データを表示しているだけのシンプルな内容になっています。
create
resources/js/Pages/Post/Create.jsx
import React from 'react'; import PostForm from "@/Components/Post/Form"; export default function Create(props) { const { post, errors } = props; return ( <PostForm type="create" post={post} errors={errors}></PostForm> ); }
edit
とフォームを共通化してます。(「コンポーネントをつくる」で紹介します)
edit
resources/js/Pages/Post/Edit.jsx
import React from 'react'; import PostForm from "@/Components/Post/Form"; export default function Edit(props) { const { post, errors } = props; return ( <PostForm type="edit" post={post} errors={errors}></PostForm> ); }
create
とフォームを共通化してます。(「コンポーネントをつくる」で紹介します)
コンポーネントをつくる
続いて、ビューの中でセットしたコンポーネントが存在していないので、それぞれつくっていきます。
フォーム
create
とedit
で表示する入力フォームはほとんど同じなので、コンポーネントにして共通化します。
というのも、共通化しておくと、もし変更があっても1つのファイルだけを触ればOKになり、後でラクできるからです(← コレ大事 😊👍)
resources/js/Components/Post/Form.jsx
import React, { useState } from 'react'; import Label from "@/Components/Label"; import {Link, useForm} from "@inertiajs/inertia-react"; import {Inertia} from "@inertiajs/inertia"; import Loading from "@/Components/Loading"; import _ from 'lodash'; export default function Form(props) { // Data const type = props.type; // create or edit const post = props.post; const errors = props.errors; const { data, setData } = useForm({ title: _.get(post, 'title', ''), body: _.get(post, 'body', ''), }); const [loading, setLoading] = useState(false); // Methods const onFinish = () => setLoading(false); const onSubmit = () => { setLoading(true); if(type === 'create') { // 登録 or 変更で切り替える const url = route('post.store'); Inertia.post(url, data, { onFinish }); } else if(type === 'edit') { const url = route('post.update', props.post.id); Inertia.put(url, data, { onFinish }); } }; return ( <div className="p-4"> <div className="mb-3"> <Label>タイトル</Label> <input className="w-80 border border-gray-500 p-2 rounded" value={data.title} onChange={e => setData('title', e.target.value)} /> {errors.title && <div className="mt-2 text-red-500 bg-red-100 p-2 rounded">{errors.title}</div>} </div> <div className="mb-3"> <Label>本文</Label> <textarea className="w-80 border border-gray-500 p-2 rounded" value={data.body} onChange={e => setData('body', e.target.value)} /> {errors.body && <div className="mt-2 text-red-500 bg-red-100 p-2 rounded">{errors.body}</div>} </div> <button type="button" className="text-white bg-blue-700 rounded-lg text-sm px-4 py-2 mr-5" onClick={onSubmit}>送信する</button> <Link href={route('post.index')}>戻る</Link> <br /> <Loading show={loading}></Loading> </div> ); }
内容としては、フォームでデータを管理し、送信するという流れになっています。
また、Lodash
はこういうとき便利ですね👍
ページリンク
では、次にページリンク(pagination
)です。
resources/js/Components/Pagination.jsx
import React from 'react'; import NavLink from "@/Components/NavLink"; export default function Pagination({ data }) { return ( <div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6"> <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between"> <div> <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px"> {data.total > 0 && data.links.map(link => ( <NavLink key={link.label} href={link.url} active={link.active}> <span dangerouslySetInnerHTML={{ __html: link.label }} /> </NavLink> ))} </nav> </div> </div> </div> ); }
ここでは、Laravel
のpaginate()
から取得できるデータ構造に合わせてページリンクをつくっています。
ローディング(送信中かどうかが分かる表示)
送信時に何もリアクションがないと、「ん…送信されてる??」と不安になるので、完了するまでの間「読み込み中…」と表示するようにします。
なお、クルクル回るアイコンを使うと、よりリアルタイム感があっていいですね👍
resources/js/Components/Loading.jsx
import React from 'react'; export default function Loading({ show = false }) { return ( <> {show && <span>読み込み中...</span>} </> ); }
ここでは、show
がtrue
かfalse
かで表示/非表示を切り替えているだけです。
ルートをつくる
では、最後にルートです。
以下を追加してください。
routes/web.php
// 省略 Route::prefix('post')->controller(PostController::class)->group(function(){ Route::get('/', 'index')->name('post.index'); Route::get('/create', 'create')->name('post.create'); Route::post('/', 'store')->name('post.store'); Route::get('/{post}', 'show')->name('post.show'); Route::get('/{post}/edit', 'edit')->name('post.edit'); Route::put('/{post}', 'update')->name('post.update'); Route::delete('/{post}', 'destroy')->name('post.destroy'); }); // 省略
作業はここで完了です。
お疲れ様でした😊✨
テストしてみる
では、実際にテストしてみましょう❗
まずはブラウザで「http://******/post」へアクセスします。
はい❗ページ一覧が表示されました。
では、まずはデータを登録するために「+ 追加する」ボタンをクリックしてみます。
すると、入力フォームが表示されるので、以下のようにして送信ボタンを押してみます。
すると・・・・・・
ページが移動して完了メッセージが表示されました。
では、ページリンクの最後まで移動してデータを確認してみましょう。
はい❗先ほど登録したデータを確認することができました。
では、次にこのデータで変更機能のチェックをしてみます。
すぐ右側にある「変更」ボタンをクリックします。
すると、先ほどと同じフォーム(共通化している部分)が表示されるので、中身を変更して送信してみましょう。
すると・・・・・・
変更完了メッセージが表示されました。
では、今回もページ移動してきちんとデータ変更できているか確認してみましょう。
はい❗
先ほどの変更が反映されています。
では、続いて確認ページです。
すぐ右側にある「確認」ボタンをクリックします。
すると・・・・・・
はい❗先ほど変更した内容が表示されました。
では、最後に削除です。
先ほどに一覧ページに戻って「削除」ボタンをクリックします。
アラート確認がでるので「OK」ボタンをクリックすると・・・・・・
はい❗
削除完了メッセージが表示され、先ほどまで表示されていたデータが消えました。
すべて成功です😊✨
企業様へのご提案
これはReact
に限らずですが、今回のように共通部分のコンポーネントをつくり、別の場所でも利用しやすくしておくことで、開発のスピードをあげることができます。
また、CSSフレームワークも同じものをつかっている場合でしたら、複数のプロジェクト間でもコンポーネントを再利用しやすいため、さらに開発効率を上げることができるでしょう。
ぜひそういった開発をご希望でしたら、いつでもお気軽にご相談ください。
お待ちしております。😊✨
おわりに
ということで、今回は「Laravel + Vite + Inertia + React」でCRUD
機能をつくってみました。
なお、私は長年「Vue派」だったのでReact
はあまり使って来ませんでしたが、昔チャレンジしたときよりはゼンゼン好印象でした。
「バーチャルDOM」のclassName
はちょっと嫌かなとは思うのですが、全体的に整理整頓されたコードが書けますし、なによりVite
を使っているのでnpm
のときのようなビルド時間が不要というのも大きな理由の1つです。
また、少し前にlivewire
を使ってみましたが、個人的な印象としてはそっちに近いのかな、という印象を受けました。
ぜひ皆さんもVue
との違いを楽しんでみてくださいね。
ではでは〜❗
「ワーケーションしたら、
生活リズムが
バラバラになってしまいました…」