Inertia.jsとは?Laravel+ReactでCRUD機能を実装してみた
目次
Inertia.jsの仕組みと、Laravel+Reactスターターキットで実際にタスク管理のCRUD機能を実装・検証した記録です 。
Laravel + React での開発を調べていたら、「Inertia.js」というライブラリがよく出てきました。
サーバーサイドのルーティング・コントローラーはそのまま使いながら、画面遷移だけを SPA のようにサクサクにできる仕組みだそうです。
REST API を別途作らなくても、Laravel のコントローラーが返し た値がそのまま Vue や React の props になるらしく、フロントとバックエンドを別々に開発・デプロイする必要がなくなるとのことでした。
気になったので、実際に Docker 上で Laravel + React の環境を作り、タスク管理の CRUD 機能を実装してみました。
サードパーティアダプターが公式サイトに一覧化されている主要なフレームワーク
Inertia.js は名前の知名度もあって Laravel 専用のツールだと思われがちですが、実際にはフレームワーク非依存(framework-agnostic)な設計です。
Inertia.js 公式サイトの Community Adapters ページによると、Inertia 本体が公式サポートしているサーバーサイドアダプターは Laravel のみですが、コミュニティ製のサードパーティアダプターとして以下のようなフレームワーク向けのものが一覧化されています。
- Rails (Ruby)
- Django (Python)
- Phoenix (Elixir)
- Symfony (PHP)
- NestJS (Node.js)
その他、Go・Rust・WordPress 向けのアダプターまで挙げられており、対応の幅はかなり広いです。
クライアントサイドは Vue・React・Svelte に公式対応しています。
今回は Laravel + React の組み合わせで検証しました。
サーバーサイドルーティングのまま SPA 的な体験を実現する仕組み
Inertia.js 公式サイトには、"Develop React, Vue, and Svelte SPAs with the elegance of server-side routing. Plug and play with any backend, meticulously optimized for Laravel. No API required."と書かれています。
意訳すると「サーバーサイドルーティングのエレガントさのまま、React・Vue・Svelte で SPA を開発できる。どんなバックエンドとも組み合わせられ、特に Laravel に最適化されている。API は不要」という説明です。
通常の SPA 開発では
- バックエンド側に REST API や GraphQL のエンドポイントを用意する
- フロントエンド側に Vue Router や React Router などのルーティングを用意する
- 取得したデータをフロント側の状態管理(Pinia、Redux など)に載せる
という 3 層の実装が必要になりますが、Inertia を使うとルーティングとコントローラーは Laravel 側にそのまま残し、コントローラーが返したデータが直接 Vue/React の props として渡ってきます(この仕組みは後述の実装で実際に確認できました)。
Inertia.js のメリット
一番の恩恵:API を作らずに props が直接届く
素の Vue(Vue Router + axios)でよくある実装だと、画面ごとに「取得中かどうか」「取得できたか」「エラーが起きたか」の状態管理が必要になります。
<!-- 素のVueの場合 -->
<script setup>
import { ref, onMounted } from "vue"
import axios from "axios"
const tasks = ref([])
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const res = await axios.get("/api/tasks")
tasks.value = res.data
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
</script>
<template>
<p v-if="loading">読み込み中...</p>
<p v-else-if="error">エラーが発生しました</p>
<ul v-else>
<li v-for="task in tasks" :key="task.id">{{ task.title }}</li>
</ul>
</template>Inertia の場合、コントローラーが返したtasksがそのまま props に入ってくるので、このloading/errorのハンドリングがまるごと不要になります。
<!-- Inertiaの場合 -->
<script setup>
defineProps<{ tasks: { id: number; title: string }[] }>()
</script>
<template>
<ul>
<li v-for="task in tasks" :key="task.id">{{ task.title }}</li>
</ul>
</template>API エンドポイントの設計・実装・ドキュメント化も不要になるので、コード量だけでなく設計判断の数自体が減ります。
その他のメリット
- Laravel の認証・認可・バリデーションの仕組み(
FormRequestなど)をそのまま使い回せる routes/web.phpに定義したルートがアプリの唯一のルーティングになるので、フロントとバックエンドでルート定義が二重管理にならない- サーバーサイド MVC の開発体験(Blade での開発に近い書き味)のまま、画面遷移だけがフルリロードなしになる
従来の SPA のデメリットと Inertia
従来型の SPA(Vue Router や React Router でフロントアプリを API から完全に切り離す構成)には、いくつか運用上のコストがありました。
- バックエンドとフロントエンドで別々のリポジトリ・別々のデプロイパイプラインが必要になりがち
- 同じバリデーションルールをバックエンド(サーバー)とフロントエンド(クライアント)の両方に実装しがちで、ズレが起きやすい
- 認証まわりを API トークンや SPA 向けのセッション連携(Sanctum など)で別途 組む必要がある
- SEO や OGP 生成のために、SSR(サーバーサイドレンダリング)の仕組みを別途用意する必要が出てくることが多い
Inertia は、こうした「SPA を作りたいが、API 層をフルで作る手間やコストは避けたい」というニーズから生まれたツールだと理解しています。
サーバーサイドは今までどおり Laravel のコントローラー・ルーティング・Blade に近い感覚で書きつつ、ブラウザ上の体験だけを SPA 化する、いわば「モノリシックなサーバーサイド MVC で、SPA の見た目を後付けする」アーキテクチャです。
Docker 上で Laravel + React(Inertia.js)のタスク管理 CRUD を実装した
公式ドキュメントを読むだけでなく、Docker 上に Laravel + React のスターターキットで環境を作り、実際に動くタスク管理の CRUD 機能を実装してみました。
1. React + Inertia スターターキットでプロジェクト作成
laravel new inertia-react-demo --react --database=sqlite --no-authentication --pest --no-node --no-interaction執筆時点で Laravel 13.29.0 + inertiajs/inertia-laravel v3.3.1 がインストールされました。
フロント側は@inertiajs/react 3.7.0、React 19.2.8 です。
2. app.tsx がかなりシンプルだった
生成されたresources/js/app.tsxは、以下のように驚くほど短いコードでした。
import { createInertiaApp } from "@inertiajs/react"
const appName = import.meta.env.VITE_APP_NAME || "Laravel"
void createInertiaApp({
title: title => (title ? `${title} - ${appName}` : appName),
progress: {
color: "#4B5563",
},
})resolveやsetupの指定がありません。
不思議に思って@inertiajs/react本体のソース(node_modules/@inertiajs/react/dist/index.js)を読んでみたところ、createInertiaAppは内部でresolve(name, page)を無条件に呼び出しており、resolveを渡さなければ動かないコードになっていました。
つまりどこかでresolveが補われているはずだと思い、vite.config.tsで読み込まれている@inertiajs/viteプラグイン(inertia())側のソースも確認しました。
こちらは Vite の AST パーサーでソースコード中のcreateInertiaApp(...)呼び出しを直接検出し、ビルド時にresolve関数を注入するコード変換を行っていました。
// node_modules/@inertiajs/vite/dist/index.js (抜粋)
return `resolve: async (name, page) => {
${transformLine}const pages = import.meta.glob(${glob}${globOptions})
...
}`つまりapp.tsxに書かれているcreateInertiaApp({...})は、ビルド時に@inertiajs/viteプラグインによってソースコードごと書き換えられ、resources/js/pages配下をimport.meta.globで解決するresolve関数が自動で差し込まれる仕組みでした。
一般的な Vite 設定ファイルとしての「プラグイン」という理解を超えて、ソースコードそのものを AST レベルで書き換えるタイプのプラグインだったのは意外でした。
3. tasks テーブルとモデルを作成
php artisan make:model Task -mrcマイグレーションにtitleとis_doneカラムを追加しました。
// database/migrations/xxxx_create_tasks_table.php
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->boolean('is_done')->default(false);
$table->timestamps();
});4. コントローラーを実装
indexでタスク一覧を Inertia の props として渡し、store/update/destroyは普通の Laravel のコントローラーとして実装しました。
// app/Http/Controllers/TaskController.php
public function index(): Response
{
return Inertia::render('tasks/index', [
'tasks' => Task::latest()->get(),
]);
}
public function store(Request $request): RedirectResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
]);
Task::create($validated);
return redirect()->route('tasks.index');
}ルートはRoute::resourceで一括登録しました。
// routes/web.php
Route::resource('tasks', TaskController::class)->only(['index', 'store', 'update', 'destroy']);5. Wayfinder でルートの型付きヘルパーが自動生成された
このスターターキットにはLaravel Wayfinderが最初から組み込まれており、npm run buildを実行すると、コントローラーのアクションに対応した TypeScript の関数が自動生成されました。
// resources/js/actions/App/Http/Controllers/TaskController.ts (自動生成)
export const store = (
options?: RouteQueryOptions
): RouteDefinition<"post"> => ({
url: store.url(options),
method: "post",
})これのおかげで、フロント側で URL 文字列をベタ書きせずに済みました。
import TaskController from "@/actions/App/Http/Controllers/TaskController"
post(TaskController.store.url())試しにroutes/web.phpのRoute::resource(...)->only([...])からupdateを外してビルドし直してみると、生成されるTaskController.tsからupdate関数がまるごと消え、tsc --noEmitが「updateというプロパティは存在しない」というエラーで検知してくれました。
ルートを消し忘れたままフロント側だけ古い呼び出しが残る、という事故を型で防げるのは便利だと感じました。
6. React ページを実装
useFormでフォームの状態管理と送信をまとめて扱えます。
// resources/js/pages/tasks/index.tsx
const { data, setData, post, processing, errors, reset } = useForm({
title: "",
})
const submit: FormEventHandler = e => {
e.preventDefault()
post(TaskController.store.url(), {
onSuccess: () => reset("title"),
})
}タスクごとの完了トグル・削除も、行ごとにuseFormを持たせて実装しました。
function TaskRow({ task }: { task: Task }) {
const {
data,
setData,
put,
delete: destroy,
processing,
} = useForm({
title: task.title,
is_done: task.is_done,
})
const toggleDone = () => {
setData("is_done", !data.is_done)
put(TaskController.update.url(task.id), { preserveScroll: true })
}
const remove = () => {
destroy(TaskController.destroy.url(task.id), { preserveScroll: true })
}
// ...
}公式ドキュメントによると、preserveScroll: trueを指定すると、更新後の画面遷移でスクロール位置がリセットされないとのことです。
今回はタスクの数が少なくスクロールが発生しない画面だったので、この挙動自体は未検証です。
タスクの追加・完了・削除を実際に操作して検証した
php artisan serveでサーバーを立ち上げ、ヘッドレスブラウザで実際にタスクの追加・完了・削除を行いながらスクリーンショットを撮りました。
初期状態です。タスクはまだ 0 件です。
「Inertia.js の記事を書く」を追加すると、ページ全体がリロードされることなく一覧に反映されました。
続けて「スクリーンショットを撮る」も追加しました。
チェックボックスを押すと、完了状態がサーバーに保存され、取り消し線が表示されます。
「削除」を押すと、そのタスクが一覧から消えます。
一連の操作の間、ブラウザのアド レスバーはずっと/tasksのままで、いわゆる「白画面のリロード」は一度も発生しませんでした。
実際の通信を見てみた
ブラウザの通信をログに取って、Inertia が裏側で何をやっているか確認しました。
通常のブラウザで直接/tasksにアクセスしたときは、<script data-page="app" type="application/json">というタグの中にページのデータが JSON として埋め込まれた、普通の HTML が返ってきました。
{
"component": "tasks/index",
"props": {
"tasks": [{ "id": 2, "title": "スクリーンショットを撮る", "is_done": true }]
},
"url": "/tasks",
"version": "e1c0a3ff3ef00d02e406de33da10c8a4"
}一方、画面遷移時に Inertia のクライアントが送るリクエストにはX-Inertia: trueヘッダーが付いており、レスポンスは HTML 全体ではなく、上記と同じ形の純粋な JSON(Content-Type: application/json)だけが返ってきました。
| リクエストの種類 | X-Inertiaヘッダー | レスポンスの中身 |
|---|---|---|
| 直接アクセス・リロード | なし | 通常の HTML(JSON を埋め込んだ<script>タグ入り) |
| 画面遷移(SPA 的な遷移) | true | ページ本体を含まない、素の JSON |
初回だけ普通の HTML として返し、以降の画面遷移は JSON のやり取りに切り替える、という単純な仕組みで SPA 的な体験を実現していることが実際の通信からも確認できました。
Inertia.js のデメリット
実際に触ってみて感じたデメリットは以下の通りです。
- サーバーサイドとフロントエンドが密結合になるため、フロントだけを別チームで完全に切り離して開発する、というスタイルには向いていません
- モバイルアプリなど、Web 以外のクライアントに同じ API を使い回したい場合には、結局別途 REST API を用意する必要があります
- ページ単位でコントローラーから props を渡す設計になるため、複数ページで同じデータを使い回したいときの共通化は自分で工夫が必要 です(共有データは
HandleInertiaRequestsのミドルウェアで一括して渡すことはできます)
逆に言うと、「バックエンドとフロントエンドを同じチーム・同じリポジトリで完結させたい」というプロジェクトであれば、これらはあまりデメリットにならないと思います。
まとめ
Inertia.js を実際に触ってみて、「SPA を作りたいけれど、API 層をフルスクラッチで設計・実装するコストは避けたい」というときに、ちょうどいい落としどころを提供してくれるツールだと感じました。
Laravel 専用のツールだと思っていましたが、公式サポートは Laravel のみであるものの、Rails・Django などコミュニティ製のサードパーティアダプターが公式サイトに数多く一覧化されており、考え方自体はフレームワークに依存しないアーキテクチャパターンだということが分かったのも収穫でした。
コントローラーの戻り値がそのままフロントの props になる体験は、Blade での開発に近い感覚のまま SPA 的な UX が手に入るので、既存の Laravel プロジェクトに SPA 的な画面をあとから足したい場合にも向いていると思います。




