公開日 2026-04-09

LaravelでInertia + React を始める

Laravel 13 の fresh app に Inertia.js と React を導入し、middleware、root template、React 側の入口、Book 一覧ページまで独立構成で確認する。

目次

  1. 1. ゴールと非対象
  2. 2. 先に全体像をつかむ
  3. 3. fresh app と起動確認を進める
  4. 3-1. 作業ディレクトリを作成して fresh app を展開する
  5. 3-2. compose.yml を作成する
  6. 3-3. .env の SQLite 設定を更新する
  7. 4. Inertia + React の入口をつなぐ
  8. 4-1. 依存を導入して middleware を生成する
  9. 4-2. bootstrap/app.php に HandleInertiaRequests を追加する
  10. 4-3. root template を作成する
  11. 4-4. React 側の入口を作成する
  12. 4-5. vite.config.js を React 向けに更新する
  13. 5. Book 一覧ページを作る
  14. 5-1. モデルと controller の雛形を作る
  15. 5-2. app/Models/Book.php を更新する
  16. 5-3. books テーブルの migration を作る
  17. 5-4. controller で Inertia ページを返す
  18. 5-5. React ページコンポーネントを作る
  19. 5-6. ルートを追加する
  20. 6. 確認とつまずきやすい点
  21. 6-1. migrate、tinker、route:list、build を通す
  22. 6-2. ブラウザで /books を確認する
  23. 6-3. 詰まりやすい点を先に切り分ける
  24. 7. まとめ

Blade の画面は追えても、Inertia を入れる段階になると middleware、root template、React 側の入口、Vite の入力ファイルが同時に出てきます。どこを変えれば Inertia::render() が React コンポーネントへ届くのか、最初の 1 回は見通しを持ちにくい箇所です。

この記事では、Laravel 13 の fresh app を空ディレクトリから作り、Book 一覧ページを Inertia + React で返すところまで進めます。既存記事の成果物は前提にせず、compose.yml.env、依存導入、artisantinkerroute:listnpm run build、ブラウザ確認までを 1 本で通します。

対象読者: Laravel の Route / Controller / View / artisan は追えるが、Inertia + React を fresh app からどう始めるかで止まりやすい PHP 学習者・実務者。

コマンドは、特記がない限り WSL 側の Ubuntu ターミナルで実行します。Windows の PowerShell ではなく、bash 互換シェルを前提にします。

1. ゴールと非対象

この記事で到達する状態は次のとおりです。

  • Laravel 13 の fresh app に Inertia.jsReact を導入できる
  • HandleInertiaRequestsbootstrap/app.php に追加する
  • resources/views/app.blade.phpresources/js/app.jsx の役割を説明できる
  • /booksBook 一覧ページを返し、tinkerroute:list、ブラウザで確認できる

焦点は Inertia 導入の最小導線にあります。次の内容は対象外です。

  • Breeze / Starter Kit の導入
  • 認証付き画面
  • useForm を使った保存処理
  • SSR、テスト、CI、デプロイ
  • PostgreSQLMySQL への切り替え

2. 先に全体像をつかむ

作業ディレクトリは ~/projects/laravel-inertia-react-intro-demo に固定します。そこで fresh app を作り、compose.yml.env を整え、Laravel 側と React 側の接続点を順番に埋めていく流れです。

flowchart LR
    A[fresh app 作成] --> B[compose.yml と .env を整える]
    B --> C[Inertia と React を導入する]
    C --> D[HandleInertiaRequests を追加する]
    D --> E[root template と app.jsx を用意する]
    E --> F[Book 一覧ページを作る]
    F --> G[migrate と tinker でデータを入れる]
    G --> H[route:list / build / browser で確認する]

BladeLivewireInertia の位置づけを先に整理したい場合は、LaravelでBlade / Livewire / Inertia をどう使い分けるか が補助導線になります。Vue 版の導入手順と比較したい場合は、LaravelでInertia + Vue.js を始める が補助導線です。どちらも前提ではありません。

3. fresh app と起動確認を進める

3-1. 作業ディレクトリを作成して fresh app を展開する

開始位置は ~/projects 配下です。まず作業ディレクトリを作成し、composer:2 コンテナから fresh app を展開します。

mkdir -p ~/projects/laravel-inertia-react-intro-demo
cd ~/projects/laravel-inertia-react-intro-demo
code .

docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app composer:2 create-project laravel/laravel .

-u "$(id -u):$(id -g)" を付けているのは、生成ファイルの所有者を WSL 側の自分にそろえるためです。ここを外すと、あとで composer requirenpm install をしたときに root 所有のファイルが混ざりやすくなります。

3-2. compose.yml を作成する

この記事のホスト側ポートは 8000 です。compose.yml は次の内容で作成します。

services:
  app:
    image: composer:2
    user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
    working_dir: /app
    environment:
      HOME: /tmp
      XDG_CONFIG_HOME: /tmp/.config
      COMPOSER_HOME: /tmp/.composer
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    command: sh -lc "php artisan serve --host=0.0.0.0 --port=8000"

  node:
    image: node:24-bookworm
    user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
    working_dir: /app
    volumes:
      - ./:/app
    command: sh -lc "tail -f /dev/null"

appphp artisan serve 用、nodenpm installnpm run build 用です。この構成なら、ホスト側へ Node.js を直接入れなくても docker compose exec node ... だけで進められます。

3-3. .env の SQLite 設定を更新する

.env の更新箇所は次の 3 行です。DB_CONNECTION=sqlite は fresh app の既定値ですが、DB_DATABASE はコメントアウトされているため、コンテナ内から見える絶対パスへ固定します。

APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_DATABASE=/app/database/database.sqlite

続いて起動します。

export LOCAL_UID="$(id -u)"
export LOCAL_GID="$(id -g)"
docker compose up -d
curl -I http://localhost:8000
  • 期待結果: HTTP/1.1 200 OK

4. Inertia + React の入口をつなぐ

4-1. 依存を導入して middleware を生成する

導入コマンドは次の 4 つです。

docker compose exec app composer require inertiajs/inertia-laravel
docker compose exec node npm install react react-dom @inertiajs/react
docker compose exec node npm install -D @vitejs/plugin-react
docker compose exec app php artisan inertia:middleware

このコマンドで app/Http/Middleware/HandleInertiaRequests.php が生成されます。再実行時は Middleware already exists. と表示されるだけなので、初回導入が済んでいれば問題ありません。

4-2. bootstrap/app.phpHandleInertiaRequests を追加する

bootstrap/app.php の更新内容は次のとおりです。

<?php

use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->web(append: [
            HandleInertiaRequests::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

Inertia::render() で返したページ情報を web middleware に流すための設定です。ここを入れずに root template だけ作っても、共有 props やアセットのバージョン判定が働かず、更新時の整合が崩れやすくなります。

4-3. root template を作成する

resources/views/app.blade.php は次の内容で作成します。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title inertia>{{ config('app.name', 'Laravel') }}</title>
        @vite(['resources/css/app.css', 'resources/js/app.jsx'])
        @inertiaHead
    </head>
    <body class="bg-slate-50 text-slate-900 antialiased">
        @inertia
    </body>
</html>

確認するのは次の 3 点です。

  • @vite(...) の入力ファイルを resources/js/app.jsx に合わせること
  • @inertiaHead を head 内に置くこと
  • @inertia を body の描画位置に置くこと

@inertia は React アプリがマウントされる場所です。ここが欠けると、HTML は返っても画面本体が描画されません。

4-4. React 側の入口を作成する

resources/js/app.jsx は次の内容にします。

import './bootstrap';
import '../css/app.css';
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createRoot } from 'react-dom/client';

const appName = 'Laravel Inertia React Demo';

createInertiaApp({
    title: (title) => (title ? `${title} - ${appName}` : appName),
    resolve: (name) =>
        resolvePageComponent(
            `./Pages/${name}.jsx`,
            import.meta.glob('./Pages/**/*.jsx'),
        ),
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />);
    },
    progress: {
        color: '#2563eb',
    },
});

createInertiaApp() が React 側の入口です。Laravel 側で Inertia::render('Books/Index') を返すと、resolvePageComponent()import.meta.glob('./Pages/**/*.jsx') の結果から一致するファイルを探し、resources/js/Pages/Books/Index.jsx を読み込みます。

4-5. vite.config.js を React 向けに更新する

vite.config.js は次の内容に更新します。

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        react(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.jsx'],
            refresh: ['resources/views/**', 'resources/js/**', 'routes/**'],
        }),
        tailwindcss(),
    ],
});

@vitejs/plugin-react を追加したうえで、Laravel 側の入力ファイルも app.jsx にそろえる設定です。ここが app.js のままだと、npm run build のあとに Laravel が resources/js/app.jsx を探す一方で manifest には app.js 側の情報しかなくなり、Unable to locate file in Vite manifest: resources/js/app.jsx で止まります。

5. Book 一覧ページを作る

5-1. モデルと controller の雛形を作る

まず Artisan で雛形を生成します。

docker compose exec app php artisan make:model Book -m
docker compose exec app php artisan make:controller BookPageController

5-2. app/Models/Book.php を更新する

app/Models/Book.php は次のとおりです。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = [
        'title',
        'author_name',
        'price',
    ];
}

今回は tinker --execute から Book::query()->create(...) で 3 件投入するため、$fillable を先に設定しておきます。

5-3. books テーブルの migration を作る

database/migrations/*_create_books_table.php は次の内容に更新します。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table): void {
            $table->id();
            $table->string('title');
            $table->string('author_name');
            $table->unsignedInteger('price');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('books');
    }
};

5-4. controller で Inertia ページを返す

app/Http/Controllers/BookPageController.php は次のとおりです。

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Inertia\Inertia;
use Inertia\Response;

class BookPageController extends Controller
{
    public function __invoke(): Response
    {
        return Inertia::render('Books/Index', [
            'books' => Book::query()
                ->orderBy('title')
                ->get(['id', 'title', 'author_name', 'price']),
        ]);
    }
}

Laravel 側で返す 'Books/Index' が、React 側の resources/js/Pages/Books/Index.jsx と対応します。

5-5. React ページコンポーネントを作る

先にディレクトリを作成します。

mkdir -p resources/js/Pages/Books

resources/js/Pages/Books/Index.jsx の内容は次のとおりです。

export default function Index({ books }) {
    return (
        <main className="min-h-screen bg-slate-50 px-6 py-12">
            <div className="mx-auto max-w-4xl space-y-8">
                <header className="space-y-3">
                    <p className="text-sm font-semibold uppercase tracking-[0.2em] text-sky-700">
                        Inertia + React
                    </p>
                    <h1 className="text-3xl font-bold text-slate-900">Book 一覧</h1>
                    <p className="text-sm leading-7 text-slate-600">
                        Laravel から受け取った props を React コンポーネントで描画しています。
                    </p>
                </header>

                <section className="grid gap-4 md:grid-cols-2">
                    {books.map((book) => (
                        <article
                            key={book.id}
                            className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
                        >
                            <h2 className="text-lg font-semibold text-slate-900">{book.title}</h2>
                            <p className="mt-2 text-sm text-slate-600">著者: {book.author_name}</p>
                            <p className="mt-1 text-sm text-slate-600">
                                価格: {Number(book.price).toLocaleString('ja-JP')}円
                            </p>
                        </article>
                    ))}
                </section>
            </div>
        </main>
    );
}

5-6. ルートを追加する

routes/web.php は次の内容にします。

<?php

use App\Http\Controllers\BookPageController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/books', BookPageController::class)->name('books.index');

6. 確認とつまずきやすい点

6-1. migrate、tinker、route:list、build を通す

ここまでの設定がそろったら、次の順で確認します。

docker compose exec app php artisan migrate

docker compose exec app php artisan tinker --execute="App\\Models\\Book::query()->create(['title' => 'Domain-Driven Design', 'author_name' => 'Eric Evans', 'price' => 6200]);"

docker compose exec app php artisan tinker --execute="App\\Models\\Book::query()->create(['title' => 'Refactoring', 'author_name' => 'Martin Fowler', 'price' => 5800]);"

docker compose exec app php artisan tinker --execute="App\\Models\\Book::query()->create(['title' => 'Clean Architecture', 'author_name' => 'Robert C. Martin', 'price' => 4200]);"

docker compose exec app php artisan route:list --path=books

docker compose exec app php artisan tinker --execute="dump(App\\Models\\Book::query()->orderBy('title')->get(['title', 'author_name', 'price'])->toArray());"

docker compose exec node npm run build

最初の 3 本は、Book::query()->create(...) を 1 件ずつ実行してサンプルデータを入れるコマンドです。tinker の期待結果は、そのあとにタイトル昇順で 3 件の配列が出ることです。検証では次の順になりました。

  • Clean Architecture
  • Domain-Driven Design
  • Refactoring

route:list --path=books では、次のように books.index が見えれば十分です。

GET|HEAD  books ........................ books.index › BookPageController

6-2. ブラウザで /books を確認する

ビルド後に次の URL を開きます。

  • http://localhost:8000/books

Book 一覧 の見出しと 3 冊のカードが見えれば成功です。

Book 一覧ページが表示された状態

ブラウザを開けない環境では、HTML 側に Inertia の初期データが入っているかを curl で確認できます。

curl -s http://localhost:8000/books | sed -n '1,20p'

この出力で script data-page="app" の JSON に component":"Books\/Index"books の配列が入っていれば、Laravel 側から React ページへ props が渡っている状態です。

6-3. 詰まりやすい点を先に切り分ける

/books が開くが画面が崩れる、または白い

まず確認する場所:

  • bootstrap/app.phpHandleInertiaRequests::class を追加したか
  • resources/views/app.blade.php@inertiaHead@inertia があるか
  • resources/js/app.jsxvite.config.js の入力ファイル名が一致しているか

Unable to locate file in Vite manifest: resources/js/app.jsx が出る

主な原因:

  • npm run build をまだ実行していない
  • vite.config.jsinputresources/js/app.js のまま残っている
  • @vite(...) 側が resources/js/app.jsx へそろっていない

MassAssignmentException が出る

主な原因:

  • app/Models/Book.php$fillable を入れていない
  • tinker$fillable 更新前に実行している

7. まとめ

この手順で、Laravel 13 の fresh app に Inertia + React を導入し、/books で一覧ページを返すところまで通せました。HandleInertiaRequests を web middleware に追加し、root template の @inertia と React 側の app.jsxvite.config.js まで同じ入口にそろえると、Inertia::render() の戻り先を迷わず追えます。

次に進むなら、位置づけの整理は LaravelでBlade / Livewire / Inertia をどう使い分けるか、Vue 版との比較は LaravelでInertia + Vue.js を始める がつながりやすい導線です。フォーム送信まで進める段階では、useForm を使った保存処理を別記事で切り出すと追いやすくなります。

シリーズ 16/16

このシリーズ

Laravelの基本を最初から通す

  1. 1. Laravel入門(Route / Controller / View / Model 最小構成)
  2. 2. Laravelで最小CRUDを作る(一覧 / 作成 / 編集 / 削除)
  3. 3. Laravelで品質ゲートを敷く(PHPUnit / Larastan / Pint / GitHub Actions)
  4. 4. Laravelで認証を足す(Breeze 最小導入)
  5. 5. Laravelで認可を入れる(Policyで自分の本だけ編集できるようにする)
  6. 6. LaravelでQueueを始める(database queue + worker 最小構成)
  7. 7. Laravelでスケジューラを動かす(Command + Scheduler 最小構成)
  8. 8. Laravelでファイルアップロードを扱う(Storage + validation)
  9. 9. Laravelでリレーションを扱う(User / Book / Category の基本)
  10. 10. Laravelで検索・並び替え・ページネーション付き一覧を作る
  11. 11. LaravelでLivewireを始める
  12. 12. LaravelでLivewire一覧画面を作る(検索・並び替え・ページネーション)
  13. 13. LaravelでSanctum認証APIを作る
  14. 14. LaravelでBlade / Livewire / Inertia をどう使い分けるか
  15. 15. LaravelでInertia + Vue.js を始める
  16. 16. LaravelでInertia + React を始める 現在の記事