まだ触り始めて1ヶ月ぐらい。困りが発生する程度には使ってきたとは思う。
Inertia.js とは
主に Laravel コミュニティで管理されている OSS。ひと言で言うとフロントエンドをモダンに書けるようになるアダプタ……かなぁ。GitHub でも 7000 star 集めているぐらい。ちょうど先週 v3.0.0 が出ましたね。元気に開発されています。
概要は僕もこの後説明するけど、作者のブログ記事 を見るのが一番イメージ沸くと思う。
間違いなくいつもの MPA
Inertia.js は、古き良き Web アプリケーションフレームワークの View 層のみをモダンな UI フレームワークに差し替えられるもの、という理解をしている。決定的なのは Routing をサーバが持つこと、レスポンスをサーバが返しているかのような感覚で書けることだろう。
具体例は 公式サイトのファーストビュー が十分に分かりやすい。
いつもの Controller で、最後に Inertia::render。描画するコンポーネント (Users) と props (users) を渡している。
<?php # UserController.php class UsersController { public function index() { $users = User::query() ->active() ->orderBy('name') ->get(['id', 'name', 'email']); return Inertia::render('Users', [ 'users' => $users, ]); } }
View 側はいつもの Vue で、props を受け取っている。
<!-- Users.vue --> <script setup lang="ts"> import { Link } from '@inertiajs/vue3' defineProps<{ users: User[] }>() </script> <template> <div v-for="user in users" :key="user.id"> <Link :href="`/users/${user.id}`"> {{ user.name }} </Link> <p>{{ user.email }}</p> </div> </template>
MPA の概念のまま、サーバから props を View に渡すところを受け持っているのが Inertia.js。フロントエンドは Vue, React, Svelte に対応しています。
フロントエンドの体験を良くしやすい
ここまでだと react-rails で render component: 'Users', props: { users: users } したのと同じでは、と思うかもしれない。
Inertia.js の体験が良いのは、サーバ側のアダプタであるだけでなく、「フロントエンドのフレームワーク」でもある点が大きい。
クライアントでは <Link> コンポーネントを使って遷移する。Link をクリックすると、XHR して SPA 遷移する。
render :inertia がリクエストの方法に応じて出し分けるようになっていて、
- 初回 HTML だと
<div id="app" data-page='{"component":"...","props":{...}, ...}'>という HTML を render し、component と props を渡す *1 - XHR だと JSON のみを返す
JSON が返ってきたときにその component を render するのはフロントエンド側の Inertia.js 側が受け持っている。そしてこの仕組みは X-Inertia ヘッダの有無によってサーバが自動で render し分けている。シンプル。
Rails に長く触れてきた人には「Turbolinks のツラみを解消している」と言えば伝わるだろうか。PJAX を推し進めて、JSON を返し分けるようにしたことで、UI フレームワークとの間をもっと上手く取り持っている。
Inertia.js はサーバ側の規約と、クライアント側のフレームワークなんです。
主権はサーバにある
フロントエンドは routing を持たず、ただサーバにリクエストを投げる。
ページ遷移をするときは
import { Link } from "@inertiajs/react"; <Link href="/">Home</Link>;
で遷移する。これはサーバに fetch で request を投げ、サーバが response となる { component, props } を返して、クライアントはそれに従って描画する。
Flash 表示をしたければいつも通りに Flash に詰めたら、クライアント側でその Flash が描画される。
Form もサーバにクライアントサイドから POST される。サーバはいつも通り扱うだけでいい。validation についても以下のようにいつも通りに書くだけ。
class UsersController def create user = User.new(user_params) # いつもの感じで validation して if user.save redirect_to users_url, notice: "作成しました" else # 失敗したら errors を詰めて form に返す redirect_to new_user_url, inertia: { errors: user.errors } end end
これで Form 側では props に errors が入っているし、Form の state はクライアント側で維持されている。フロントエンドのフレームワークであるというのが効いていて、ページ遷移として POST しているのではなくクライアントサイドで request を投げているので、response を受け取ったときの動きが上手く統合されている。
deferred props という仕組みがあるのも面白い。重い処理は deferred にして page は先に render してしまい、クライアント側から追加で取りに行く。これをサーバ側の指定のみで書くことができる。
render inertia: "users/index", props: { users: User.all, stats: inertia.defer { HeavyQuery.run } }
「component 名と props をサーバから渡す」から想像できるよりも、何を描画するか、どう描画するかの主権はずっとサーバ側にあると感じるんじゃないかな。これが一番の特徴だろう。
他の選択肢との比較
API + Frontend や、Next.js と比べたいって声は当然あると思う。
「モノリスである」「主権はサーバにあり page 単位で考える」が大きな違いと感じている。
例えば session はサーバ側でいつものように管理するし、データの fetch は page 単位 (=controller が常に起点)。バリデーションも認可もサーバ側でいつもの感じで書く。本当にいつも通りで最高。
co-location の書き味では GraphQL や RSC に大きく劣るだろう。欲しいデータをクライアント側から指定することはできない。page 単位で props が渡されるのを持ち回さないといけない。*2
API 境界がない (props を動的に受け渡しているだけだし、どんな props を受け渡すのかの静的定義は用意されていない) というのも違いになり得ると思う。が、後述するように明らかにツラすぎるのでここはまず定義することになる。
Inertia.js を採用するときの決め手は、サーバを API サーバとしてではなくページ遷移を中心として捉えるかどうかだと思う。 あくまでサーバが主で、クライアントは View と割り切れるチームだとちょうどいい。クライアントサイドでアプリケーションを構築するもの、サーバはデータを提供する API、と考えている人が多いチームだと厳しい。
フロントエンド専属チームを用意しない人数規模に最適だと思っていて、サーバ起点の設計を維持しながら、モダンフロントエンドフレームワークも組み込みたいときには強く選択肢に挙がると感じた。
Pjax/Hotwire/htmx との違いについては、サーバは HTML 断片ではなく props を渡すのでモダンフロントエンドと同居できる、が一番の違い。次に Hotwire というか Turbo Frames とは「HTML 断片ではなく page 全体を返す」という違いがある。部分更新ではないという意味では Turbo Drive に近いんだけど、かなりレンダリングに介入できるので Turbo Drive よりフロントエンドを扱いやすい。
その他、色々気になるポイント
クライアントとサーバの間で何を渡すのかの定義は揃える
フロントエンドに型が無いと書きづらいのは、これはさすがに当然そうです。文化がそうなっている。
型が無くても開発できるのは DB の型を知っているからというのはあって、フロントエンドには DB をそのまま露出するわけにもいかないので型定義がここで途切れる。露出するコンポーネント (DB からフロントエンドに Serializer 層で変換したもの) の型は定義すべき。
自分では OpenAPI Spec を書く (committee gem を使った validation はできないんだけれど) という選択肢を採った。将来的に API サーバに転生する未来はありそうだし、Spec を書いておいて損は無いだろう。 *3
逆に Serializer 層から型定義を生成するのは https://github.com/skryukov/typelizer を使うといいんじゃないか。私は使ったことないのでオススメ度合いも微妙なところなんだけれど。
reverse routing は必要
routes をサーバに持つ、ということでフロントエンド側で request path を組み立てる仕組みはデフォルトでは用意されていないんだけど、これもさすがに無いと生きるのがツラい。
サーバ側の routes を SSoT とした URL 組み立て (=named routes) をフロントエンドでも使える仕組みが必要になる。幾つか選択肢があるけど私は js-routes gem を使いました。
SSR がデフォルトで用意されていて助かる
https://inertiajs.com/docs/v3/advanced/server-side-rendering
- サーバサイドが HTML layout と component, props を返す
- SSR サーバが component, props を元に HTML を rendering する
- サーバサイドで SSR サーバからの response をマージした HTML にする
- SSR された HTML がクライアントに返る
みたいな感じ。
JSON を返す X-Inertia request に対しては SSR されないし、する必要も無いというのは非常に自然で好き。
ページ単位なので props が肥大化する
MPA として考えたら描画に必要なものだけなので特に差が無いんじゃないか。サーバで HTML をレンダリングしているという発想に戻せば困らない。
なんだけど、当然サイドバーを常に毎回描画し直す必要無いよねとかはあって、そういうのもちゃんと用意されているので工夫しましょう。*4
ドキュメントを LLM に渡しやすい
https://inertiajs.com/docs/ は各ページに「Copy page」ボタンがあり、クリックすると markdown がクリップボードに入る。
https://inertiajs.com/docs/llms.txt の内容ですね。
Evil Martians
これは Rails の人向け。不安を減らす理由の一つになるんじゃないか。
(日本語訳: Rails: Inertia.jsでRailsのJavaScript開発にシンプルさを取り戻そう(翻訳)|TechRacho by BPS株式会社)
inertia-rails 自体の積極開発もそうだけど、alba-inertia や typelizer も開発していて、意思が見えるので乗っかりやすい。
まとめ
Inertia.js はサーバ側は規約 (なので Laravel 以外にも Rails や Django やらなんやかんやがある)。フロントエンドは Pjax をかなり推し進めたもの。
あまりにもいつも通り書けるからビックリすると思う。Rails の 7 つのアクションをそのまま使っていて構わない。これでいいんだよ。
「画面を見れば DB 設計と RESTful URL がイメージ付く」ぐらいまで訓練されてしまった Rails 戦士にとって、Inertia.js を使ったアプリケーション開発は、いつも通りのサーバサイドを書きながらモダンフロントエンドを導入できるいい選択肢です。