ホーム>Laravel>Inertia.jsとは?Laravel+ReactでCRUD機能を実装してみた
Laravel

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",
  },
})

resolvesetupの指定がありません。

不思議に思って@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

マイグレーションにtitleis_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.phpRoute::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 01 initial

「Inertia.js の記事を書く」を追加すると、ページ全体がリロードされることなく一覧に反映されました。

inertia js 02 added

続けて「スクリーンショットを撮る」も追加しました。

inertia js 03 added second

チェックボックスを押すと、完了状態がサーバーに保存され、取り消し線が表示されます。

inertia js 04 toggled

「削除」を押すと、そのタスクが一覧から消えます。

inertia js 05 deleted

一連の操作の間、ブラウザのアドレスバーはずっと/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 的な画面をあとから足したい場合にも向いていると思います。

フィードバックのお願い
この記事のフィードバックがありましたらYoutubeの適当な動画にコメントしていただいたり、お問い合わせからご連絡ください。