🫣

Inertia React×Laravel

2024/08/01に公開

Inertia.jsとは

Inertia.jsは、サーバーサイドとクライアントサイドの両方で使えるJavaScriptライブラリです。LaravelやRailsなどのサーバーサイドフレームワークと組み合わせて、SPA(Single Page Application)を構築する際に便利です。Inertia.jsは、サーバーサイドでのデータ処理やルーティングを行い、クライアントサイドではReactやVue.jsなどのフレームワークを使用して、フロントエンドの開発を行うことができます。

1. 前提条件

  • Node.js
  • Composer
  • PHP

2. 導入手順

2.1 Laravelプロジェクトのセットアップ

まず、Laravelプロジェクトを作成します。

$ composer create-project laravel/laravel inertia-react-app
$ cd inertia-react-app

2.2 Laravel Breeze/Inertia.jsのインストール

Laravel Breezeは、Inertiaフロントエンド実装により、ReactとVueを使用するスカフォールドも提供しているため、Breezeをインストールします。詳細はBreezeとReact/Vueをご参照ください。
今回は、Reactを使用するため、以下のコマンドを実行します。

$ composer require laravel/breeze --dev
$ yarn install
$ php artisan breeze:install react --typescript
$ composer require inertiajs/inertia-laravel

3. Inertia.jsの記法 (TypeScript)

3.1 Inertia.jsルーティングの設定

Breezeの設定では、以下のようにInertia.jsのルーティングが設定されています。

// routes/web.php
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/dashboard', function () {
    return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__.'/auth.php';

3.2 Inertia.jsのコントローラーの作成

Breezeによって自動的に生成されるProfileControllerの例を見てみましょう。

// app/Http/Controllers/ProfileController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;

class ProfileController extends Controller
{
    public function edit(Request $request)
    {
        // resources/js/Pages/Profile/Edit.tsxをレンダリング。
        // propsとしてuserを渡す。
        return Inertia::render('Profile/Edit', [
            'user' => $request->user(),
        ]);
    }

    public function update(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255',
        ]);

        Auth::user()->update($request->only('name', 'email'));

        return redirect()->route('profile.edit');
    }

    public function destroy(Request $request)
    {
        Auth::user()->delete();

        return redirect('/');
    }
}

3.3 TypeScriptでのReactコンポーネントの作成

Breezeのインストールにより、Inertia.jsの設定がReactで行われます。これをTypeScriptで設定します。

// resources/js/Pages/Dashboard.tsx
import React from 'react';

const Dashboard: React.FC = () => {
    return (
        <div>
            <h1>Dashboard</h1>
        </div>
    );
};

export default Dashboard;
// resources/js/Pages/Profile/Edit.tsx
import React from 'react';
import { useForm } from '@inertiajs/react';

interface Props {
    user: {
        name: string;
        email: string;
    };
}

const EditProfile: React.FC<Props> = ({ user }) => {
    const { data, setData, patch } = useForm({
        name: user.name,
        email: user.email,
    });

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        patch(route('profile.update'));
    };

    return (
        <div>
            <h1>Edit Profile</h1>
            <form onSubmit={handleSubmit}>
                <div>
                    <label htmlFor="name">Name</label>
                    <input
                        id="name"
                        type="text"
                        value={data.name}
                        onChange={(e) => setData('name', e.target.value)}
                    />
                </div>
                <div>
                    <label htmlFor="email">Email</label>
                    <input
                        id="email"
                        type="email"
                        value={data.email}
                        onChange={(e) => setData('email', e.target.value)}
                    />
                </div>
                <button type="submit">Save</button>
            </form>
        </div>
    );
};

export default EditProfile;

4. リダイレクト

Inertia.jsを使用している場合、リダイレクトの方法にはいくつかの選択肢があります。

4.1 return Inertia::location(route('profile.edit'));

  • Inertia特有のリダイレクト方法: この方法はInertia.jsを使用している場合に特有のリダイレクト方法です。
  • クライアントサイドでのリダイレクト: このリダイレクト方法は、JavaScriptを介してクライアントサイドでリダイレクトを処理します。つまり、サーバーサイドからのリダイレクトではなく、クライアントサイドでURLが変更され、ページが再読み込みされます。
  • SPA体験の維持: この方法は、シングルページアプリケーション(SPA)体験を維持するために使用されます。通常のリダイレクトとは異なり、ページ全体のリロードを避け、部分的なリダイレクトを可能にします。

4.2 return redirect()->route('profile.edit');

  • 通常のLaravelのリダイレクト方法: こちらはLaravelの通常のリダイレクト方法です。
  • サーバーサイドでのリダイレクト: このリダイレクトは、サーバーサイドでHTTPリダイレクトを発生させます。ブラウザは新しいURLに対して完全に新しいリクエストを行い、ページ全体がリロードされます。
  • フルリダイレクト: 通常のHTTPリダイレクトであり、完全なページリロードが発生します。これにより、アプリケーションのSPA体験が一時的に中断されることがあります。

4.3 使い分け

  • Inertia.jsのリダイレクト (Inertia::location): SPA体験を維持したい場合や、Inertia.jsのページ間で部分的な遷移を行いたい場合に使用します。ページの一部だけが変更され、全体のリロードが発生しません。
  • 通常のLaravelリダイレクト (redirect()->route): フルページリロードが許容される場合や、Inertia.jsを使用していない場合に使用します。標準的なHTTPリダイレクトを発生させます。

// Inertia.jsのリダイレクト
public function update(Request $request)
{
    // バリデーションと更新処理
    // ...

    // Inertia.jsを使用したリダイレクト
    return Inertia::location(route('profile.edit'));
}

// 通常のLaravelリダイレクト
public function update(Request $request)
{
    // バリデーションと更新処理
    // ...

    // 通常のLaravelリダイレクト
    return redirect()->route('profile.edit');
}

5. Inertia.jsのLinkコンポーネント

Inertia.jsのLinkコンポーネントは、シングルページアプリケーション(SPA)の体験を提供するための主要な要素の一つです。このコンポーネントを使用することで、ページ遷移時にフルページリロードを避け、部分的な更新のみでスムーズなユーザー体験を提供できます。

5.1 基本的な使い方

まずは、基本的な使い方を見てみましょう。

import React from 'react';
import { Link } from '@inertiajs/react';

const Dashboard: React.FC = () => {
    return (
        <div>
            <h1>Dashboard</h1>
            <Link href={route('profile.edit')}>Edit Profile</Link>
        </div>
    );
};

export default Dashboard;

上記の例では、Linkコンポーネントを使って「Edit Profile」ページへのリンクを作成しています。href属性には、Laravelのrouteヘルパーを使って動的に生成したURLを渡しています。

5.2 特徴とプロパティ

Linkコンポーネントは通常の<a>タグと似た使い方ができますが、いくつかの追加プロパティと特徴があります。

  • href: リンク先のURLを指定します。通常の<a>タグと同様です。
  • method: リクエストメソッドを指定します。デフォルトはGETですが、POSTPUTPATCHDELETEなども指定できます。
  • data: リクエスト時に送信するデータを指定します。フォームデータなどを渡す際に使用します。
  • as: リンクのHTMLタグを指定します。デフォルトは<a>ですが、<button>など他のタグを使用することもできます。
  • replace: trueにすると、履歴スタックを置き換えます(window.history.replaceStateを使用)。

5.3 例: リクエストメソッドを変更

以下は、Linkコンポーネントを使ってDELETEリクエストを送信する例です。

import React from 'react';
import { Link } from '@inertiajs/react';

const UserList: React.FC = () => {
    const users = [
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Doe' }
    ];

    return (
        <div>
            <h1>User List</h1>
            <ul>
                {users.map(user => (
                    <li key={user.id}>
                        {user.name}
                        <Link
                            href={route('user.destroy', user.id)}
                            method="delete"
                            as="button"
                        >
                            Delete
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
};

export default UserList;

この例では、ユーザーリストを表示し、各ユーザーに対してDELETEリクエストを送信するLinkコンポーネントを使っています。methodプロパティでdeleteを指定し、asプロパティで<button>タグとして表示しています。

5.4 例: フォームデータの送信

フォームデータを送信する際にもLinkコンポーネントを使用できます。

import React from 'react';
import { Link } from '@inertiajs/react';

const CreateUser: React.FC = () => {
    const [name, setName] = React.useState('');

    return (
        <div>
            <h1>Create User</h1>
            <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Enter user name"
            />
            <Link
                href={route('user.store')}
                method="post"
                data={{ name }}
                as="button"
            >
                Create
            </Link>
        </div>
    );
};

export default CreateUser;

この例では、ユーザー名を入力し、そのデータをPOSTリクエストで送信するリンクを作成しています。dataプロパティを使用して、送信するデータを指定しています。

6. Inertia.jsのuseFormについて

useFormはInertia.jsが提供するReactのカスタムフックで、フォームの状態管理と送信を簡単に行うためのものです。このフックを使うことで、フォームの入力値を管理し、バリデーションや送信処理を簡潔に実装できます。

6.1 基本的な使い方

まずは基本的な使い方を見てみましょう。

import React from 'react';
import { useForm } from '@inertiajs/react';

const CreateUser: React.FC = () => {
    const { data, setData, post, processing, errors } = useForm({
        name: '',
        email: '',
    });

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        post(route('users.store'));
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="name">Name</label>
                <input
                    id="name"
                    type="text"
                    value={data.name}
                    onChange={(e) => setData('name', e.target.value)}
                />
                {errors.name && <div>{errors.name}</div>}
            </div>
            <div>
                <label htmlFor="email">Email</label>
                <input
                    id="email"
                    type="email"
                    value={data.email}
                    onChange={(e) => setData('email', e.target.value)}
                />
                {errors.email && <div>{errors.email}</div>}
            </div>
            <button type="submit" disabled={processing}>Create</button>
        </form>
    );
};

export default CreateUser;

6.2 プロパティとメソッド

useFormフックが返すオブジェクトにはいくつかのプロパティとメソッドがあります。
下記、一部紹介します。

  • data: フォームのデータオブジェクト。初期値を渡してフォームの入力値を管理します。
  • setData: フォームデータを更新する関数。setData('fieldName', value)の形式で使用します。
  • post/put/patch/delete/get: フォームを送信するためのメソッド。HTTPメソッドに応じて適切なものを選択します。
  • processing: フォームが送信中かどうかを示すブール値。送信中はtrueになります。
  • errors: バリデーションエラーメッセージを格納するオブジェクト。サーバーサイドからのエラーを表示するのに使用します。
  • reset: フォームデータをリセットする関数。
  • clearErrors: エラーメッセージをクリアする関数。

6.3 詳細な例

さらに詳細な例を見てみましょう。ここでは、ユーザーのプロフィールを編集するフォームを作成します。

import React from 'react';
import { useForm } from '@inertiajs/react';

interface Props {
    user: {
        name: string;
        email: string;
    };
}

const EditProfile: React.FC<Props> = ({ user }) => {
    const { data, setData, put, processing, errors, reset } = useForm({
        name: user.name,
        email: user.email,
    });

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        put(route('profile.update'), {
            onSuccess: () => reset(),
        });
    };

    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label htmlFor="name">Name</label>
                <input
                    id="name"
                    type="text"
                    value={data.name}
                    onChange={(e) => setData('name', e.target.value)}
                />
                {errors.name && <div>{errors.name}</div>}
            </div>
            <div>
                <label htmlFor="email">Email</label>
                <input
                    id="email"
                    type="email"
                    value={data.email}
                    onChange={(e) => setData('email', e.target.value)}
                />
                {errors.email && <div>{errors.email}</div>}
            </div>
            <button type="submit" disabled={processing}>Update</button>
        </form>
    );
};

export default EditProfile;

6.4 ポイント

  1. フォームデータの初期化: フォームデータの初期値をuseFormフックに渡して管理します。
  2. 入力値の更新: setData関数を使用してフォームの入力値を更新します。
  3. フォームの送信: postputなどのメソッドを使用してフォームを送信します。routeヘルパーを使ってLaravelのルートを指定します。
  4. バリデーションエラーの表示: サーバーサイドから返されるエラーメッセージをerrorsオブジェクトから取得して表示します。
  5. 送信中の状態管理: processingプロパティを使って送信中のボタンを無効にする

などの処理を行います。
6. フォームのリセット: reset関数を使ってフォームをリセットします。

これにより、Inertia.jsを使ったフォームの状態管理と送信が効率的に行えます。

7. 環境変数の使用方法

Inertia.jsを使用してLaravel BreezeとReactを組み合わせる際、環境変数の使用方法を2つの方法でまとめてみます。

7.1 方法1: .envをconst.phpで定義し、Controllerで呼び出して、propsで受け取る

7.1.1 .envに環境変数を定義

APP_NAME=Laravel
APP_URL=http://localhost

7.1.2 const.phpを作成

環境変数をPHPの定数として定義します。

// config/const.php

return [
    'app_url' => env('APP_URL', 'http://localhost'),
];

7.1.3 コントローラーで環境変数を渡す

コントローラーで環境変数をInertiaのpropsとして渡します。

// app/Http/Controllers/HomeController.php

namespace App\Http\Controllers;

use Inertia\Inertia;

class HomeController extends Controller
{
    public function index()
    {
        return Inertia::render('Home', [
            'appUrl' => config('const.app_url'),
        ]);
    }
}

7.1.4 Reactコンポーネントで受け取る

Reactコンポーネントでpropsとして受け取ります。

// resources/js/Pages/Home.tsx

import React from 'react';

interface Props {
    appUrl: string;
}

const Home: React.FC<Props> = ({ appUrl }) => {
    return (
        <div>
            <h1>Home Page</h1>
            <p>App URL: {appUrl}</p>
        </div>
    );
};

export default Home;

7.2 方法2: vite-env.d.tsでinterfaceを定義して、.envでVITE_APP_BASE_URLとして定義し、const.tsを作成してコンポーネントで呼び出す

7.2.1 .envに環境変数を定義

APP_NAME=Laravel
APP_URL=http://localhost
VITE_APP_BASE_URL="${APP_URL}"

7.2.2 vite-env.d.tsを作成

Viteの環境変数の型定義を行います。

// resources/js/types/vite-env.d.ts

/// <reference types="vite/client" />

interface ImportMetaEnv {
    readonly VITE_APP_BASE_URL: string;
}

interface ImportMeta {
    readonly env: ImportMetaEnv;
}

7.2.3 const.tsを作成

環境変数をエクスポートするファイルを作成します。

// resources/js/const.ts

export const APP_BASE_URL = import.meta.env.VITE_APP_BASE_URL;

7.2.4 Reactコンポーネントで呼び出す

エクスポートした環境変数をReactコンポーネントで使用します。

// resources/js/Pages/Home.tsx

import React from 'react';
import { APP_BASE_URL } from '../const';

const Home: React.FC = () => {
    return (
        <div>
            <h1>Home Page</h1>
            <p>App Base URL: {APP_BASE_URL}</p>
        </div>
    );
};

export default Home;

8. まとめ

Inertia.jsを用いることで、LaravelとReactをシームレスに統合し、SPAのような体験を提供しながらサーバーサイドの利便性を維持することができます。本記事では、Inertia.jsの基本的な使い方、ルーティングの設定、コントローラーの作成、Reactコンポーネントの作成、リダイレクト方法、Linkコンポーネント、useFormフック、環境変数の使用方法について説明しました。
詳しくは、Inertia.js公式ドキュメントを参照してください。

個人的には、Reactを単体で使用すると、どうしても外部ライブラリの使用が増えてしまいます。その結果、グループ開発では各メンバーの知識やスキルに依存することが多くなり、足並みを揃えることが難しくなります。
公式ドキュメントでもフレームワークの使用を勧めています。Reactプロジェクトを始める
Reactのフレームワークでは、Next.jsが有力候補の認識ですが、普段Laravelを使用することが多いので、このInertia.jsという選択肢は非常に魅力的に感じました。

Discussion