【7日目】React初学者が1週間でどこまで出来るか挑戦(CRUD総まとめ)

はじめに

React学習1週間チャレンジ7日目(最終日)です。これまでReactの公式ドキュメントを中心に基礎的な部分を学んできました。

「どこまで出来るか挑戦」の結果は以下の通りです。Inertiaを使う前提ですが基礎を理解した上でCRUD処理は実装できました。

振り返り:

  • Breezeパッケージを使ってInertiaとReactをLaravelに導入
  • Breezeを使わず、InertiaとReactを導入
  • Inertiaの使い方
  • Reactの基礎をドキュメントで学習(2チャプター完了)
  • CRUD処理の実装【今回】

今回は1週間の復習も兼ねて、より実践的なケースを想定したCRUD処理を作ってみました。

環境:

  • Mac M1 2020
  • Docker: 24.0.2
  • PHP: 8.2.x
  • Laravel: 10.x
  • MySQL: 5.7
  • nginx: 1.25.1
  • React: 18.2.0

実践: 顧客マスタのCRUDを実装

顧客マスタを想定してCRUD処理を作ってみます。(user_idは前回までに作ったusersテーブルと紐付けます)今回は間に合いませんでしたが、user_idを選択するために非同期でユーザー検索モーダル画面も今後実装する予定です。

Index: 一覧表示

customersテーブルを定義

  Schema::create('customers', function (Blueprint $table) {
      $table->id();
      $table->foreignId('user_id');
      $table->string('name');
      $table->string('address');
      $table->string('phone_number');
      $table->string('email');
      $table->timestamps();
  });

Customerモデルの設定

// app/Models/Customer.php

protected $fillable = [
    'user_id',
    'name',
    'address',
    'phone_number',
    'email',
];

public function user(): BelongsTo
{
    return $this->belongsTo(User::class);
}

ルーティング定義

// routes/web.php
Route::get('customers', [CustomerController::class, 'index'])->name('customers.index');

indexメソッドを定義(CustomerController)

// app/Http/Controllers/CustomerController.php

use App\Models\Customer;
use Inertia\Inertia;

...

public function index()
{
    return Inertia::render('Customer/Index', [
        'customers' => Customer::with('user')->get(),
    ]);
}

Viewの作成(Index.jsx

// resources/js/Pages/Customer/Index.jsx
import AppLayout from '@/Layouts/AppLayout';

export default function Index({ customers }) {
  return (
    <AppLayout>
      <h1 className="title title-h1">顧客マスタ</h1>
      <table className="table">
        <thead className="table-header">
          <tr>
            <th className="th-cell">ID</th>
            <th className="th-cell">名前</th>
            <th className="th-cell">住所</th>
            <th className="th-cell">電話番号</th>
            <th className="th-cell">E-mail</th>
            <th className="th-cell">担当ユーザーID : 名前</th>
          </tr>
        </thead>
        <tbody>
          {customers.map(customer => (
            <tr key={customer.id}>
              <td className="td-cell">{customer.id}</td>
              <td className="td-cell">{customer.name}</td>
              <td className="td-cell">{customer.address}</td>
              <td className="td-cell">{customer.phone_number}</td>
              <td className="td-cell">{customer.email}</td>
              <td className="td-cell">{customer.user_id} : {customer.user.name}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </AppLayout>
  );
}

コードを管理しやすくするために、テーブルはコンポーネントに切り出します。CustomerTableコンポーネントを作成:

// resources/js/Pages/Customer/Partials/CustomerTable.jsx
export default function CustomerTable({ customers }) {
  return (
    <table className="table">
      <thead className="table-header">
        <tr>
          <th className="th-cell">ID</th>
          <th className="th-cell">名前</th>
          <th className="th-cell">住所</th>
          <th className="th-cell">電話番号</th>
          <th className="th-cell">E-mail</th>
          <th className="th-cell">担当ユーザーID : 名前</th>
        </tr>
      </thead>
      <tbody>
        {customers.map(customer => (
          <tr key={customer.id}>
            <td className="td-cell">{customer.id}</td>
            <td className="td-cell">{customer.name}</td>
            <td className="td-cell">{customer.address}</td>
            <td className="td-cell">{customer.phone_number}</td>
            <td className="td-cell">{customer.email}</td>
            <td className="td-cell">{customer.user_id} : {customer.user.name}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Index.jsxCustomerTableコンポーネントを読み込み:

// resources/js/Pages/Customer/Index.jsx
import AppLayout from '@/Layouts/AppLayout';
import CustomerTable from "./Partials/CustomerTable";

export default function Index({ customers }) {
  return (
    <AppLayout>
      <h1 className="title title-h1">顧客マスタ</h1>
      <CustomerTable customers={customers} />
    </AppLayout>
  );
}

一覧画面のページ完成。

Create: 登録フォーム

ルーティング定義

// routes/web.php
Route::get('customers/create', [CustomerController::class, 'create'])->name('customers.create');

コントローラーの設定

// app/Http/Controllers/CustomerController.php

use Inertia\Inertia;

...

public function create()
{
    return Inertia::render('Customer/Create');
}

Index.jsxの適当な場所に登録フォームへのリンクを作成。tighten/ziggyを使ってJSX内でLaravelのrouteへルパを使用している。

// resources/js/Pages/Customer/Index.jsx
<a href={route('customers.create')} className="btn btn-create">新規登録</a>

登録フォーム(Create.jsx)を作成。InertiaのuseFormへルパを使用。

// resources/js/Pages/Customer/Create.jsx
import AppLayout from '@/Layouts/AppLayout';
import { useForm } from '@inertiajs/react';

export default function Create() {
  const { data, setData, post, errors, processing, isDirty } = useForm({
    user_id: '',
    name: '',
    address: '',
    phone_number: '',
    email: '',
  });

  const submit = (e) => {
    e.preventDefault();
    post(route('customers.store'))
  };

  return (
    <AppLayout>
      <h1 className="title title-h1">顧客 登録</h1>
      <form onSubmit={submit}>

        <div className="row">
          <input type="text"
            name="name"
            value={data.name}
            onChange={(e) => setData('name', e.target.value)}
            className="input-field"
            placeholder="名前"
          />
          <div className='invalid-feedback'>{errors.name}</div>
        </div>

        <div className="row">
          <input type="text"
            name="address"
            value={data.address}
            onChange={(e) => setData('address', e.target.value)}
            className="input-field"
            placeholder="住所"
          />
          <div className='invalid-feedback'>{errors.address}</div>
        </div>

        <div className="row">
          <input type="text"
            name="phone_number"
            value={data.phone_number}
            onChange={(e) => setData('phone_number', e.target.value)}
            className="input-field"
            placeholder="電話番号"
          />
          <div className='invalid-feedback'>{errors.phone_number}</div>
        </div>

        <div className="row">
          <input type="email"
            name="email"
            value={data.email}
            onChange={(e) => setData('email', e.target.value)}
            className="input-field"
            placeholder="E-mail"
          />
          <div className='invalid-feedback'>{errors.email}</div>
        </div>

        <div className="row">
          <input type="number"
            name="user_id"
            value={data.user_id}
            onChange={(e) => setData('user_id', e.target.value)}
            className="input-field"
            placeholder="担当ユーザーID"
          />
          <div className='invalid-feedback'>{errors.user_id}</div>
        </div>

        <button className='btn btn-store' disabled={!isDirty || processing}>
          登録
        </button>
      </form>
    </AppLayout>
  );
}

Store: 登録処理

ルーティング

// routes/web.php
Route::post('customers/store', [CustomerController::class, 'store'])->name('customers.store');

バリデーション

// app/Http/Requests/CustomerStoreRequest.php
public function rules(): array
{
    return [
        'user_id'       => 'required|integer|exists:users,id',
        'name'          => 'required|string|max:255',
        'address'       => 'required|string',
        'phone_number'  => 'required|string|max:20',
        'email'         => 'required|email|unique:customers,email',
    ];
}

コントローラ

// app/Http/Controllers/CustomerController.php
use App\Http\Requests\CustomerStoreRequest;

...

public function store(CustomerStoreRequest $request)
{
    $inputs = $request->only([
        'user_id',
        'name',
        'address',
        'phone_number',
        'email',
    ]);

    Customer::create($inputs);

    return redirect()->route('customers.index');
}

Edit: 編集フォーム

ルーティング定義

// routes/web.php
Route::get('customers/{customer}/edit', [CustomerController::class, 'edit'])->name('customers.edit');

コントローラ

// app/Http/Controllers/CustomerController.php
use App\Models\Customer;
use Inertia\Inertia;

...

public function edit(Customer $customer)
{
    return Inertia::render('Customer/Edit', [
        'customer' => $customer,
    ]);
}

Viewの作成。Createとの違いは、

  • 初期値としてdataにpropsで受け取ったcustomerの値をセットしていること
  • フォーム送信先をpatch(route('customers.update', customer.id))にする

import AppLayout from '@/Layouts/AppLayout';
import { useForm } from '@inertiajs/react';

export default function Edit({ customer }) {
  const { data, setData, patch, errors, processing, isDirty } = useForm({
    user_id: customer.user_id,
    name: customer.name,
    address: customer.address,
    phone_number: customer.phone_number,
    email: customer.email,
  });

  const submit = (e) => {
    e.preventDefault();
    patch(route('customers.update', customer.id))
  };

 // 以下省略。Create.jsxと同じ。

Update: 更新処理

ルーティング

// routes/web.php
Route::patch('customers/{customer}', [CustomerController::class, 'update'])->name('customers.update');

バリデーション。CustomerStoreRequestと異なるのはuniqueルールのignore処理のみ。

// app/Http/Requests/CustomerUpdateRequest.php
use Illuminate\Validation\Rule;

...

'email' => [
    'required',
    'string',
    'email',
    'max:255',
    Rule::unique('customers')->ignore($this->route('customer')), // ここだけ
],
// 他は省略

コントローラ

// app/Http/Controllers/CustomerController.php
use App\Http\Requests\CustomerUpdateRequest;

...

public function update(CustomerUpdateRequest $request, Customer $customer)
{
    $inputs = $request->only([
        'user_id',
        'name',
        'address',
        'phone_number',
        'email',
    ]);

    $customer->update($inputs);

    return redirect()->route('customers.index');
}

Delete: 削除

ルーティング

// routes/web.php
Route::delete('customers/{customer}', [CustomerController::class, 'destroy'])->name('customers.destroy');

コントローラ

// app/Http/Controllers/CustomerController.php
public function destroy(Customer $customer)
{
    $customer->delete();

    return redirect()->route('customers.index');
}

Edit.jsxに削除フォームを追加する。InertiaのForm helperのdestroyメソッドを使用。第二引数には色々なvisit optionを指定することができる。ここでは、onBeforeというコールバックを指定して、結果がfalseの時は処理をキャンセルしている。

// resources/js/Pages/Customer/Edit.jsx

const { delete:destroy, ... } = useForm({ ... })

const deleteCustomer = (e) => {
  e.preventDefault();
  destroy(route('customers.destroy', customer), {
      onBefore: () => confirm('本当に削除しますか?'),
  });
}

return (
  {/* 省略 */}

  <form onSubmit={deleteCustomer}>
    <button className="btn btn-delete">削除</button>
  </form>
);

以上で基本的なCRUD処理は完成。

おわりに

今回Laravel、Inertiaを使用して進めてきましたが、Inertiaのお陰でかなり簡単にコードが書けている印象を受けました。そのため、React自体の理解は今後もっと深めていく必要があるとは感じてます。

1週間チャレンジは終わりですが、もちろんReactの勉強は今後も続けていきます。今後は実際にアプリ開発を通して知識を身につけていこうと思ってます。

連載記事

  1. 【1日目】React初学者が1週間でどこまで出来るか挑戦
  2. 【2日目】React初学者が1週間でどこまで出来るか挑戦(ひたすらドキュメント編)
  3. 【3日目】React初学者が1週間でどこまで出来るか挑戦(Inertiaの理解)
  4. 【4日目】React初学者が1週間でどこまで出来るか挑戦(Createフォーム作成)
  5. 【5日目】React初学者が1週間でどこまで出来るか挑戦(Createフォーム改善)
  6. 【6日目】React初学者が1週間でどこまで出来るか挑戦(イベントとState)
  7. 【7日目】React初学者が1週間でどこまで出来るか挑戦(CRUD総まとめ)