【1日目】React+Inertiaへ移行する環境を作る

投稿日 最終更新日

2025年07月25日~2025年10月12日

半年前くらいに作曲知識共有サイトをどうにか動く形にしてdtmru α版としてリリースしたんだけど、改善したいところが無限にあるので改善する。

なにをしたいのか

とりあえず、以下の2つを実行したい

  1. フロントをInertia.js + Reactへ
  2. 記事エディタをCKEditorからTipTapへ

半年間やってたこと

dtmruを少しいじったりはしてたものの、普通に関係ないプログラミングやったり、ゲームやったり、始めての一人暮らし+社会人にてんやわんやしてたりした。

そんな中、2ヶ月前くらいにClaude Codeという自律型コーディングAIが出てきた。
結構革命的で「これでInertia移行余裕やろ!」と思ったら本当にうまく行かなかったので、やっぱり私はこうやって日記形式で学んだことを吐き出しながら作業するのが向いているんだなと思って戻ってきた。

何がだめなのかわからないんだけど、Claude CodeはLaravelの実装はいけてもInertiaを使った実装がダメダメなんだよね。おそらく、Inertiaの情報が少なすぎるのが原因かなと思ったり。
あと、その時はReactへ移行しようと思ってたんだけど、Calude Codeが吐き出したReactの実装を見ても私がReactわからないので、毎回コードチェックにめっちゃ時間かかる。
そんなことするなら私が日記に知識を落としながら主導して実装していくのがいいよなぁという。

React.jsかVue.jsか

大前提としてInertiaを使いSPA + SSRへ移行することは確定している。
理由は以下

  • Laravelを使いたい
    • Laravelが好きだから
    • Laravelのスキルが既にある程度あるから
    • 既にLaravelで実装しているから
    • 仕事でも使うから
  • Laravelを完全バックエンドオンリーのAPIにはしない
    • 学習コストデカすぎなので
      • ただでさえInertiaだけでも以下を学ばなきゃいけない
        • Inertia
        • React or Vue
        • TS
      • それに加えてLaravelをAPI的に使うとなると以下を学ばなきゃいけない
        • API設計
        • SSRをするために必要なフレームワーク
    • LaravelがInertia推してるので
  • 記事エディタをTipTapにしたい
    • 現在のCKEditorが使いにくいから
    • ヘッドレスでUIを自由自在にいじれるから
    • PHP用のサニタイズライブラリがあったり、Livewire、AlpineのチュートリアルがあったりとLaravelと相性が良さげ。どっちも使わない予定だけど。
  • エディタをTipTapにするついでにフロントをモダンにしたい
    • SSR + SPAにはしたい
    • まだわからないけど、音を扱う以上、音のプレイヤーとか実装するとなったらSPAじゃなきゃだめだし、記事共有サイトという性質上、SEOのためにSSRでありたい

その上で、取れる選択肢は以下

  1. Livewire へ移行
    • Livewireを使うんだったらReactかVueを学びたいのでペケ
  2. 現行のBlade+Alpine.jsで実装
    • Blade + Alpine.jsは非常にありなんだけど、ならLivewireでよくない? となるし、先のことを考えるとSSR + SPAにしたいのでペケ
  3. LaravelをバックエンドAPI化して、Next.jsとかをフロント側にする
    • 最初はフロントとバックエンドで完全にサーバーを分けるこの形を考えていたんだけど、明らかにオーバースペックなのでペケ。あと、Next.jsを使うならもうNext.jsだけで実装しちゃいたいかも。
  4. Inertia + Reactへ移行
  5. Inertia + Vue へ移行

ということで、残りはInertia + React or Inertia + Vueという感じ。

React vs Vueの雑感

雑な感想としては

  • おそらく、どっちを採用してもやりたいことはできる
  • 学習のコストを取るか、将来の可能性を取るかという感じ
  • React
    • 好印象なとこ
      • シェアは日本・世界共にVueより上
      • Laravelとの組み合わせでもシェア上げつつある
      • エコシステムでかめ
      • Webエンジニアとして、アプリとして長い将来を考えるならReact
      • 生のjsを使うjsxが個人的にすこ
    • 懸念点
      • 規模が大きく複雑で、選択肢が多く学習・実装が長くなりそう
    • ただ、しっかり触ったこと無いのでよくわからん
  • Vue
    • 好印象なとこ
      • 全体シェアは劣るが、Laravelとの組み合わせではReactと比べて2倍のシェア
      • 2024年のフロントエンドフレームワーク満足度ではReactが75%に対し、Vueは87%
      • Vueは別プロジェクトで使っており、特に大きな不満点はない
      • 小規模〜中規模におすすめで、開発速度を求めるならこっちなイメージ
      • ロゴがネギっぽいのがGood
    • 懸念点
      • シェアと将来性。これに尽きる。
        • 正直、シェアがあって将来性もReactと同じくらいあるならVueを使うかなぁ

うーん。
こうやって文章にまとめると私が取るべき選択は明らかで、Vueだよね。

私は良いキャリアを積んでいくのが目標ではないし、個人開発だし、Vueに不満を思ったことはないし、Laravelとの相性も悪くない。

ただ、jsxは結構好きめな開発体験だし、しっかりReactをイジったことがないのでイジイジしたい。
つまり、興味があって楽しそうと感じているのはReact。だから、一回Reactを採用してみてダメそうならVueにしようかなという結論にする。

Blade + Alpine.jsからInertia.js + React.jsへ移行する

何を参考に移行するかなんだけど、Laravel12よりInertia.js + Reactのスターターキットが存在するのでこれを参考に環境を作る。

Laravel11→12

参考にするスターターキットは12基準のものなので、流石にバージョン合わせたほうがいいかなと思いLaravelのバージョンを上げる。

11→12の変更点がそもそも少なく、依存パッケージもすべてv12対応済みだったので問題なく上げられるだろうという算段。

実際私の場合は#54281の変更が既存の実装と競合というか、私も独自に空白文字を検知してバリデーションで弾く実装をしていたのでテストが思い通りに動かなくなったくらいで問題なくバージョン上げられた。

Laravel12のスターターキットの技術構成を見る

スターターキットInertia.js + React.jsの技術構成を見て、私に何が必要で何が不要なのかを判断していく。

スターターキットはlaravel/installerのv5.16のlaravel newで以下のように選択して生成されたものを参考にする。

 Which starter kit would you like to install? ────────────────┐
 React
 └──────────────────────────────────────────────────────────────┘

 Which authentication provider do you prefer? ────────────────┐
 Laravel's built-in authentication                            │
 └──────────────────────────────────────────────────────────────┘

 ┌ Which testing framework do you prefer? ──────────────────────┐
 │ PHPUnit                                                      │
 └──────────────────────────────────────────────────────────────┘

見るのはcompose.jsonとpackage.jsonの2つ。

composer.json

重要なとこだけ出すと以下のような感じ。

"require": {  
    "php": "^8.2",  
    "inertiajs/inertia-laravel": "^2.0",  
    "laravel/framework": "^12.0",  
    "laravel/tinker": "^2.10.1",  
    "tightenco/ziggy": "^2.4"  
},  
"require-dev": {  
    "fakerphp/faker": "^1.23",  
    "laravel/pail": "^1.2.2",  
    "laravel/pint": "^1.18",  
    "laravel/sail": "^1.41",  
    "mockery/mockery": "^1.6",  
    "nunomaduro/collision": "^8.6",  
    "phpunit/phpunit": "^11.5.3"  
},

Inertia + React環境特有で必要そうなのは以下の2つっぽい

  1. inertiajs/inertia-laravel
    • inertiajs/inertia-laravelはInertia使うので必要
  2. tightenco/ziggy
    • route()関数をjsで使えるようにする。便利だしハードコーディングしたくないので必要

package.jsonの中身

フロントの移行なのでこっちが大変なことになってる。
スターターキットは以下のような感じ

"devDependencies": {  
    "eslint": "^9.17.0",  
    "@eslint/js": "^9.19.0",  
    "eslint-config-prettier": "^10.0.1",  
    "eslint-plugin-react": "^7.37.3",  
    "eslint-plugin-react-hooks": "^5.1.0",  
    "typescript-eslint": "^8.23.0",  
    "prettier": "^3.4.2",  
    "prettier-plugin-organize-imports": "^4.1.0",  
    "prettier-plugin-tailwindcss": "^0.6.11",  
    "@types/node": "^22.13.5"  
},  
"dependencies": {  
    "react": "^19.0.0",  
    "react-dom": "^19.0.0",  
    "@inertiajs/react": "^2.0.0",  
    "typescript": "^5.7.2",  
    "@types/react": "^19.0.3",  
    "@types/react-dom": "^19.0.2",  
    "vite": "^7.0.4",  
    "@vitejs/plugin-react": "^4.6.0",  
    "laravel-vite-plugin": "^2.0",  
    "concurrently": "^9.0.1",  
    "globals": "^15.14.0",  
    "tailwindcss": "^4.0.0",  
    "@tailwindcss/vite": "^4.1.11",  
    "tailwindcss-animate": "^1.0.7",  
    "tailwind-merge": "^3.0.1",  
    "clsx": "^2.1.1",  
    "class-variance-authority": "^0.7.1",  
    "lucide-react": "^0.475.0",  
    "@headlessui/react": "^2.2.0",  
    "@radix-ui/react-avatar": "^1.1.3",  
    "@radix-ui/react-checkbox": "^1.1.4",  
    "@radix-ui/react-collapsible": "^1.1.3",  
    "@radix-ui/react-dialog": "^1.1.6",  
    "@radix-ui/react-dropdown-menu": "^2.1.6",  
    "@radix-ui/react-label": "^2.1.2",  
    "@radix-ui/react-navigation-menu": "^1.2.5",  
    "@radix-ui/react-select": "^2.1.6",  
    "@radix-ui/react-separator": "^1.1.2",  
    "@radix-ui/react-slot": "^1.1.2",  
    "@radix-ui/react-toggle": "^1.1.2",  
    "@radix-ui/react-toggle-group": "^1.1.2",  
    "@radix-ui/react-tooltip": "^1.1.8"  
},  
"optionalDependencies": {  
    "@rollup/rollup-linux-x64-gnu": "4.9.5",  
    "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",  
    "lightningcss-linux-x64-gnu": "^1.29.1"  
}

とにかく量が多いねぇ。
とりあえず、全て調べて解説して私にとって必要か不必要かみていく。

eslint系

devDependenciesに入っている以下のやつ。

    "eslint": "^9.17.0",  
    "@eslint/js": "^9.19.0",  
    "eslint-config-prettier": "
    ^10.0.1",  
    "eslint-plugin-react": "^7.37.3",  
    "eslint-plugin-react-hooks": "^5.1.0",  
    "typescript-eslint": "^8.23.0",  

eslintはJSの構文チェックをしてくれるやつ。

ここらへんは全部導入するつもりだったんだけど、ESlint+PrettierではなくBiomeにしたので導入しない。

prettier系

devDependenciesに入っている以下のやつ

"prettier": "^3.4.2",  
"prettier-plugin-organize-imports": "^4.1.0",  
"prettier-plugin-tailwindcss": "^0.6.11",

さっきのeslintは構文チェックで、こちらのprettierはコードの自動整形。
こちらもESLintと同様にBiomeでよいので導入しない。

@types/node

devDependenciesに入っている@types/nodeというやつ。

Node.jsのTS型を提供するもの。
開発環境でnodeを利用しており、TSも使うので必要。

React系

dependenciesに入っている以下のやつ

"react": "^19.0.0",  
"react-dom": "^19.0.0",  
"@inertiajs/react": "^2.0.0",

React採用なのでもちろん入れる。

  • react
    • 導入する
    • 本体
  • react-dom
    • 導入する
    • これreactと何が違うの? と思ったんだけど、ReactはWebブラウザだけでなくモバイルアプリとかでも利用されるので、どこで描画するかによってレンダラーを変える必要があるっぽい。もちろんここではWebブラウザでレンダリングするのでreact-domを導入する。
  • @inertiajs/react
    • 導入する
    • inertiaのReact用アダプタ。Inertia × Reactなのでもちろん必要。

typescript系

dependenciesに入っている以下のやつ

"typescript": "^5.7.2",  
"@types/react": "^19.0.3",  
"@types/react-dom": "^19.0.2",

TSをあえて採用せずJSDocで実装という判断もなくはなかったんだけど、LaravelがもうTSデフォなので思い切って採用する。

vite系

dependenciesに入っている以下のやつ

"vite": "^7.0.4",  
"@vitejs/plugin-react": "^4.6.0",  
"laravel-vite-plugin": "^2.0",

Viteに文句ないのでもちろん採用。

  • vite
    • 導入する
    • 本体
  • @vitejs/plugin-react
    • 導入する
    • なんでこれ必要なんだろうと思ったんだけど、以下のメリットがあるっぽい
      • Reactのホットリロードが早くなる
      • jsx利用時にreactをインポートする必要がなくなる
    • 正直よくわかんないけど入れておく
  • laravel-vite-plugin
    • 導入する
    • LaravelでViteを使えるようにするやつ。入れよう

concurrently

dependenciesに入っている以下のやつ

"concurrently": "^9.0.1",  

concurrentlyっていうのは同時にコマンドを動かすパッケージらしい。
これ、なんで入れているんだろうと思ったんだけど、composer.json

"dev": [  
    "Composer\\Config::disableProcessTimeout",  
    "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
    ],  
"dev:ssr": [  
    "npm run build:ssr",  
    "Composer\\Config::disableProcessTimeout",  
    "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"php artisan inertia:start-ssr\" --names=server,queue,logs,ssr"
    ],

とかに使われているので入っているっぽい。
個人的に使う予定は無いのでこれは入れなくていいかな。

globals

dependenciesに入っている以下のやつ

"globals": "^15.14.0",  

globalsっていうのは、ESLintがブラウザデフォルトで定義されているwindowsなどのグローバル変数を定義してあげるものっぽい。これがないとESLintが検知してくれないとのこと。

であれば入れる方針で問題なさそう。

tailwind系

dependenciesに入っている以下のやつ

"tailwindcss": "^4.0.0",  
"@tailwindcss/vite": "^4.1.11",  
"tailwindcss-animate": "^1.0.7",  
"tailwind-merge": "^3.0.1",  

TailwindCSS自体は採用するけど、自分には必要なさそうなものもありそう

  • tailwindcss
    • 導入する
    • 本体
  • @tailwindcss/vite
    • 導入する
    • Vite使うので
  • tailwindcss-animate
    • 導入しない
    • これはTailwindCSS公式のアニメーションを作るためのプラグインなんだけど、アニメーション使う予定ないので入れない。使うことになったら導入すればおkだと思う。
  • tailwind-merge
    • 後から必要だったら導入
    • これは、Tailiwindのクラスを競合しないようにマージするやつ。ドキュメントにもある通り、無闇矢鱈に入れればよいというわけでもないっぽい。
      一旦様子見てほしくなったら入れる方針で。

classをいい感じに操作する系

dependenciesに入っている以下のやつ

"clsx": "^2.1.1",   
"class-variance-authority": "^0.7.1",  

どちらもTailwindCSSのclassの条件とかを書きやすくしてくれるやつ。

  • clsx
    • 導入する
    • これは、特定の記法をスペース区切りの文字列に変換してくれるユーティリティ。
      falseyな値はすべて破棄されること&スペース区切りの文字列で出力されることを活かしてCSS Classの条件分岐でいい感じに使えるっぽい。
      わからんけど、入れとこ。
  • class-variance-authority
    • 後から必要だったら導入
    • 通称CVA。これは、classを生成する関数を実装してくれるライブラリ。
      バリエーションを予め定義して、使いたいバリエーションを引数に渡すことでclassを生成してくれる的な。TSにも対応している。
      これあればclsxいらないのでは? とも思うけど、よほど複雑じゃない限りはcvaでなくclsxで十分みたいなことみたい。

lucide-react

dependenciesに入っている以下のやつ

"lucide-react": "^0.475.0",

lucide-reactはlucideアイコンライブラリのReact版。
アイコンがめっちゃ簡単にいっぱい使えるよということ。

ライセンスはISC LicenseとMITの混合っぽい。ISC Licenseにあまり聞き覚えがないんだけど、MITとほぼ同等なので、著作権的なところは安心して使える。

便利そうなので、もちろん入れる。

UIライブラリ系

dependenciesに入っている以下のやつ

"@headlessui/react": "^2.2.0",  
"@radix-ui/react-avatar": "^1.1.3",  
"@radix-ui/react-checkbox": "^1.1.4",  
"@radix-ui/react-collapsible": "^1.1.3",  
"@radix-ui/react-dialog": "^1.1.6",  
"@radix-ui/react-dropdown-menu": "^2.1.6",  
"@radix-ui/react-label": "^2.1.2",  
"@radix-ui/react-navigation-menu": "^1.2.5",  
"@radix-ui/react-select": "^2.1.6",  
"@radix-ui/react-separator": "^1.1.2",  
"@radix-ui/react-slot": "^1.1.2",  
"@radix-ui/react-toggle": "^1.1.2",  
"@radix-ui/react-toggle-group": "^1.1.2",  
"@radix-ui/react-tooltip": "^1.1.8"

スターターキットには大きく分けて

  1. Headless UI
  2. Radix UI
    の2個のUIライブラリが入っている。

どちらもヘッドレスUIというジャンルのUIライブラリで、この2つがヘッドレスUIの主要ライブラリっぽい。

ヘッドレスUIは、UIの機能だけ用意して後の見た目は自由に作ってねというもの。TipTapと一緒だね。

正直、ここまで低レイヤーにするならネイティブなHTML要素でコンポーネント作成したほうがいいのでは? と思わなくもないんだけど、使ってみないと文句は言えないのでとりあえず使う。

Headless UIとTailwindCSSは同じ会社が作ってるらしく、親和性高そうなので基本的にHeadless UIを使い、Headless UIに無いものはRedix UIを探す感じでよいのかな。

optionalDependencies系

optionalDependenciesに入っている以下のやつ

"optionalDependencies": {  
    "@rollup/rollup-linux-x64-gnu": "4.9.5",  
    "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",  
    "lightningcss-linux-x64-gnu": "^1.29.1"  
}

そもそもoptionalDependenciesっていうのは、インストールに失敗してもnpmの処理を続行させたいパッケージを指定するものみたい。

ここでは〇〇-linux-x64-gnuというのが指定されているけど、これはそれぞれLinux x64 GNU用のバイナリファイルで、もしLinux x64 GNU環境だったらこれらのバイナリファイルを使って環境を作るよと宣言している感じ。

もし、Winとかの環境ならこれらのインストールには失敗して何もインストールされない。
ただ、これら3つのパッケージはなにかの依存関係で必要なものなので、Win対応版が自動的にインストールされる感じになるはず。

  • @rollup/rollup-linux-x64-gnu
    • Viteのビルドで利用するモジュールバンドラー。
      前述通り、別環境でもそれにあったものがインストールされるっぽいので記述しておいてもいいのかなという感じ。
  • @tailwindcss/oxide-linux-x64-gnu
    • TailwindCSSのlinux-x64-gnu用バイナリ。Tailwind使うから指定しとこ。
  • lightningcss-linux-x64-gnu
    • ViteのCSS圧縮で利用するCSSパーサー。Vite使うし入れとこ。

私が導入するパッケージ

そんな感じでほとんどのパッケージは入れる感じに。

かなりカオスになりそうで心配である。
なんかこれ、マイクラのmodをとりあえずいっぱい入れる時と同じ不安を感じる。

環境のインストールや初期設定やら

いきなり全部入れるとワケワカメになるので、以下のような段階的な導入を行う。

  1. React+TSの導入
  2. Inertia.jsの導入
  3. ESLint + Prettierの導入
  4. それ以外の導入

React+TSの導入

とりあえず、私のpackage.jsonに足りない以下を追加し、npm install
一部のパッケージはdevでもいいのでは説があるんだけど、今回はスターターキットに倣って全部dependenciesで。

"dependencies": {  
    "react": "^19.0.0",  
    "react-dom": "^19.0.0",
    "typescript": "^5.7.2",  
	"@types/react": "^19.0.3",  
	"@types/react-dom": "^19.0.2",
	"@vitejs/plugin-react": "^4.6.0",
	...
}
	

React導入後の設定

Laravelのドキュメントアセットの構築(Vite)を参考にしつつ初期設定を行う。

vite.config.jsに以下2行を追加

import {defineConfig} from 'vite';  
import laravel from 'laravel-vite-plugin';  
+ import react from '@vitejs/plugin-react';
...
export default defineConfig({  
    plugins: [  
        laravel({  
			...
        }),  
+       react(),
		...

更に、@viteディレクティブの前に@viteReactRefreshを追加しないといけないっぽい。
つまり、@viteが使われている場所全てで以下のようにする。

@viteReactRefresh
@vite(['resources/css/app.css', 'resources/js/app.js'])

TSの設定

tsconfig.jsonというTypeScript用の設定ファイルを作る。
これはスターターキットのものを参考に作るんだけど、理解はしておきたいので1から生成する。

npx tsc --init

で生成。

{  
  // Visit https://aka.ms/tsconfig to read more about this file  
  "compilerOptions": {  
    // File Layout  
    // "rootDir": "./src",    // "outDir": "./dist",  
    // Environment Settings    // See also https://aka.ms/tsconfig/module    "module": "nodenext",  
    "target": "esnext",  
    "types": [],  
    // For nodejs:  
    // "lib": ["esnext"],    // "types": ["node"],    // and npm install -D @types/node  
    // Other Outputs    "sourceMap": true,  
    "declaration": true,  
    "declarationMap": true,  
  
    // Stricter Typechecking Options  
    "noUncheckedIndexedAccess": true,  
    "exactOptionalPropertyTypes": true,  
  
    // Style Options  
    // "noImplicitReturns": true,    // "noImplicitOverride": true,    // "noUnusedLocals": true,    // "noUnusedParameters": true,    // "noFallthroughCasesInSwitch": true,    // "noPropertyAccessFromIndexSignature": true,  
    // Recommended Options    "strict": true,  
    "jsx": "react-jsx",  
    "verbatimModuleSyntax": true,  
    "isolatedModules": true,  
    "noUncheckedSideEffectImports": true,  
    "moduleDetection": "force",  
    "skipLibCheck": true,  
  }  
}

とりあえず初期で生成されるオプションの理解から行う。
tsconfigについての公式ドキュメントは「https://aka.ms/tsconfig」みたい。

  • compilerOptions
    • 主にこの部分へ設定を入れていく。コンパイル時の設定。
  • target
    • 出力するjsのバージョン指定。ESNextはTS最新ターゲットバージョンを意味する。
      ブラウザを対象としたjsの出力なので、ESNextでいいね。
  • types
    • どの@typesのパッケージを利用するか。無指定ですべて利用することになる。必要なものしかnpm installしてないので、無指定でいいね。
  • declaration
    • d.tsという型定義ファイルを生成するか否か。trueだとすべてのexportに対して型定義ファイルをファイルごとに作成する。
      基本的にtsファイル内に型定義もしちゃうので、webアプリ開発用途ではfalseで良さそう。
      逆に、npmでパッケージ配布するときは外部からTSを参照できるようにtrueにするっぽい。
  • declarationMap
    • .d.tsファイル用のソースマップを生成する。これがあるとIDEとかで定義元にジャンプする際に型定義ファイルではなくtsファイル自体に飛べるようになる。
      そもそも.d.tsを自動で出力しないので不要っぽい。
  • noUncheckedIndexedAccess
    • 配列やオブジェクトなどのarray[1]みたいにインデックスアクセスできる場合、インデックス先が定義されているかをチェックするか否か。
      オフにする理由もなさそうなので、trueでよさげ。
  • exactOptionalPropertyTypes
    • オプショナルなプロパティにundefinedを代入することを禁止する設定。ただ、明示的にundefinedを型宣言すれば代入できるようになる。
      なぜこれが嬉しいかは私の理解が説明できるほどではないので、公式ドキュメントを参照。
      公式が推奨している設定なのでtrueでいく。
  • jsx
    • tsxをどう処理するかの設定。
      5つくらい処理方法の選択肢があるんだけど、今回はReact 17以上から対応しているreact-jsxを指定する。どうやらreactのimportが要らなくなるらしい。
  • verbatimModuleSyntax
    • 型専用のimport/exportと普通のimport/exportの使い分けを厳格にする。
      オンでいいのでは。
  • isolatedModules
    • これは、ファイル単位でのトランスパイル結果が定まるかをチェックするか否かのルール。TSはファイル全体を見るので問題ないらしいんだけど、他のトランスパイラは1ファイルずつ処理することがあるので、その場合に対応するためのルールみたい。
      詳しくはQiitaの「TypeScriptを他のツールで取り扱うためのコンパイラオプションについて」がわかりやすかった。
  • noUncheckedSideEffectImports
    • 副作用的に使うimport先のソースファイルがない場合にエラーにするルール。
      「副作用」っていうのは、importした値や関数を使わず、読み込むだけで発揮されるコードの効果だけを得るみたいな利用方法のこと。
      これもオンでいいのでは。
  • moduleDetection
    • TSがファイルをモジュールまたはスクリプトと判断するルールの設定。
      基本的にスクリプトとしてJSは書かないので、forceにしてモジュールに強制する感じで良さそう。
  • skipLibCheck
    • 型定義ファイルのチェックをスキップするか否か。
      どうやらこれをtrueにしないとnode_modulesにある型定義ファイルでエラーが出まくるらしい。公式的にも推奨している設定なのでオンで良さそう。

続いて、スターターキットもみていく。
コメントを除外したものが以下

{  
    "compilerOptions": {  
        "target": "ESNext",  
        "module": "ESNext",  
        "moduleResolution": "bundler",  
        "allowJs": true,  
        "noEmit": true,  
        "isolatedModules": true,  
        "esModuleInterop": true,  
        "forceConsistentCasingInFileNames": true,  
  
        "strict": true,  
        "noImplicitAny": true,  
        "skipLibCheck": true,  
        "baseUrl": ".",  
        "paths": {  
            "@/*": ["./resources/js/*"],  
            "ziggy-js": ["./vendor/tightenco/ziggy"]  
        },  
        "jsx": "react-jsx"  
    },  
    "include": [  
        "resources/js/**/*.ts",  
        "resources/js/**/*.d.ts",  
        "resources/js/**/*.tsx",  
    ]  
}

ある程度かぶっているところもあるけど、初期設定にはなかったものもあるのでそれだけ見ていく

  • module
    • TSがどの形式のモジュールで出力するか。ブラウザが利用するmoduleなのでESNextなんだね
  • moduleResolution
  • allowJs
    • jsファイルをインポートできるようにする設定。段階的にJS→TSへ移行する際に使えるね。
  • noEmit
    • JSファイルをコンパイルして出力しないようにする設定。
      Viteを使っていて、Viteがコンパイルするのでtrueかな。
  • esModuleInterop
    • これをtrueにするとCommonJSもimport example from 'example;みたいにインポートできるようになって、型定義も正確になるみたい。
      つまり、CommonJSを使うパッケージとかを使うならtrueにしたい感じなのかな。
      デメリットはバンドルサイズが若干増えるとかぽい。
  • forceConsistentCasingInFileNames
    • ファイル名の大文字小文字を正確に区別するかどうかの設定。オンでいいね。
  • strict
    • TSの設定にはいくつかStrict系のルールがあるんだけど、それをすべてオンにするというもの。ストイックだね。
  • noImplicitAny
    • strict系のルールの1つ。上のstrictを有効にするとこれは自動でtrueになる。そのため、なぜこれだけ明示的に指定しているのかは謎。
      trueにすると、型注釈がない場合にエラーになる。falseだとanyで勝手に推測してくれる。
  • baseUrl
    • モジュール解決の基準ディレクトリを指定するもの。
  • paths
    • パスのエイリアス的なものを定義できる。
      "@/*": ["./resources/js/*"], とすることで、tsファイル内でimport hoge from '@/hoge'とすることで[baseUrl]/resources/js/hogeのパスを指定したのと同じことになる。

とりあえずこんなもんだろうか。

最終的に以下のようにした

{  
    // Visit https://aka.ms/tsconfig to read more about this file  
    "compilerOptions": {  
        "target": "ESNext",  
        "module": "ESNext",  
        "moduleResolution": "bundler",  
        "allowJs": true,  
        "noEmit": true,  
        "isolatedModules": true,    // ファイル単位でのトランスパイル結果が定まるかをチェックするか,  
        "esModuleInterop": true,    // CommonJSも`import example from 'example`;みたいにインポートできるようになって、型定義も正確になる  
        "forceConsistentCasingInFileNames": true,   // ファイル名の大文字小文字を正確に区別するかどうか  
  
        "strict": true,  
        "skipLibCheck": true,   // 型定義ファイルのチェックをスキップするか,  
        "noUncheckedIndexedAccess": true,   // 配列やオブジェクトなどの`array[1]`みたいにインデックスアクセスできる場合、インデックス先が定義されているかをチェックするか否か。  
        "exactOptionalPropertyTypes": true, // オプショナルなプロパティにundefinedを代入することを禁止する設定。ただ、明示的にundefinedを型宣言すれば代入できるようになる。  
        "verbatimModuleSyntax": true,   // 型専用のimport/exportと普通のimport/exportの使い分けを厳格にする。,  
        "noUncheckedSideEffectImports": true,   // 副作用的に使うimport先のソースファイルがない場合にエラーにする,  
  
        "baseUrl": ".",  
        "paths": {  
            "@/*": ["./resources/js/*"],  
            "ziggy-js": ["./vendor/tightenco/ziggy"]  
        },  
        "jsx": "react-jsx",  
        "moduleDetection": "force"  // TSがファイルをモジュールまたはスクリプトと判断する基準。forceでモジュールに強制  
    },  
    "include": [  
        "resources/js/**/*.ts",  
        "resources/js/**/*.d.ts",  
        "resources/js/**/*.tsx",  
    ]  
}

TSの設定がありすぎて疲れる。
おそらく他にも設定したほうが良いものはあるんだろうけど、ここにあるもの以外は随時必要を感じたら設定する感じでいいかなぁ。

Inertiaの導入

ReactとTSの設定が終わったので、Inertiaを入れる。

やることは以下

  • composerでInertiaのインストールと諸設定
  • npmでInertiaプラグインのインストールと諸設定
  • Ziggyではなく、Wayfinderのインストールと諸設定

ドキュメントの「サーバーサイド」と「クライアントサイド」を参考に入れる。

Composerインストール

とりあえずcomposerインストール

composer require inertiajs/inertia-laravel

テンプレートレイアウトの作成

Inertiaは初期設定でresources/views/app.blade.phpのレイアウトファイルを見に行くようになってる。
私はもともとBladeでresources/views/components/layouts/app.blade.phpのようなレイアウトファイルを作ってたんだけど、ここはInertiaに合わせに行く。

新しくresources/views/app.blade.phpを作る

<!DOCTYPE html>  
<html>  
    <head>  
        <meta charset="utf-8"/>  
        <meta name="viewport" content="width=device-width, initial-scale=1">  
        @viteReactRefresh  
        @vite(['resources/css/app.css', 'resources/js/app.js'])  
        @inertiaHead
    </head>  
    <body>  
        @inertia  
    </body>  
</html>

一応こんな感じが最小構成でないだろうか

  • viteReactRefresh
    • これを@vite()の前に入れないとReactがホットリロードでしっかり読み込まれない。日本語ドキュメントでも入れといてねくらいしか書いてないので、魔法ってことで。
  • @inertiaHead
    • この部分にReactから<head>データを送れる
  • @inertia
    • この部分にReactのページが生成される

ミドルウェアの作成

HandleInertiaRequestsというInertia用のミドルウェアを作成する。

php artisan inertia:middleware

初期でできるのが以下

<?php  
  
namespace App\Http\Middleware;  
  
use Illuminate\Http\Request;  
use Inertia\Middleware;  
  
class HandleInertiaRequests extends Middleware  
{  
    /**  
     * The root template that's loaded on the first page visit.     *     * @see https://inertiajs.com/server-side-setup#root-template  
     *     * @var string  
     */    protected $rootView = 'app';  
  
    /**  
     * Determines the current asset version.     *     * @see https://inertiajs.com/asset-versioning  
     */    public function version(Request $request): ?string  
    {  
        return parent::version($request);  
    }  
  
    /**  
     * Define the props that are shared by default.     *     * @see https://inertiajs.com/shared-data  
     *     * @return array<string, mixed>  
     */    public function share(Request $request): array  
    {  
        return [  
            ...parent::share($request),  
            //  
        ];  
    }  
}

これをwebミドルウェアグループに追加する。
Laravel10の方法なので
app/Http/Kernel.php

protected $middlewareGroups = [  
    'web' => [  
        ...        
+		\App\Http\Middleware\HandleInertiaRequests::class,  
    ],

のように追加。

テスト用のレスポンスを作成する

しっかりReactなどがInertia経由で表示されるか心配なので、テスト用の某を作る。

とりあえずweb.php

// Inertia React テスト用ルート
Route::get('/test-react', function () {
    return \Inertia\Inertia::render('Welcome', ["title" => "テスト"]);
})->name('test.react')->middleware('auth');

とか作って
resources/js/Pages/Welcome.tsx

import {Head} from '@inertiajs/react';  
  
interface Props {  
    title: string;  
}  
  
export default function Welcome({title}: Props) {  
    return (  
        <>  
            <div className="p-8 text-center">  
                <Head title={title}/>  
                <p className="text-xl text-gray-600">  
                    {title} - React移行テストページです  
                </p>  
            </div>  
        </>  
    );  
}

とかを作ればいいんじゃないかな。

  • <Head title={title}>
    • 本来は<Head>タグに囲まれた部分がテンプレートファイルの@inertiaHeadの部分へ行くんだけど、titleだけはHeadコンポーネントへ渡すことで省略できる

ちなみに、まだフロント側をいじってないのでこれで/test-reactへアクセスしても表示はされない。

npmインストール

npm install @inertiajs/react

でInertiaのReact用アダプターを入れる。

app.tsxを作成する

Inertiaを動かすため、app.tsxを作る。

app.tsxでcreateInertiaApp()という関数を実行し、Inertiaの初期化を行う。

中身は以下のような感じ

import { createInertiaApp } from '@inertiajs/react'  
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';  
import { createRoot } from 'react-dom/client'  
  
createInertiaApp({  
    resolve: (name) => resolvePageComponent(`./Pages/${name}.tsx`, import.meta.glob('./Pages/**/*.tsx')),  
    setup({ el, App, props }) {  
        createRoot(el).render(<App {...props} />)  
    },  
})

なんとなく、resloveはコンポーネントの取得、setupはInertiaの起動を行っている感じっぽい。
詳しい挙動は@inertiajs/react/dist/index.esm.jsを見るのが良き。

作ったら、テンプレートのJS読み込む部分を修正する

- @vite(['resources/css/app.css', 'resources/js/app.js'])
+ @vite(['resources/css/app.css', 'resources/js/app.tsx'])

これで、test-reactページでReactが動いたはず。

動くのを確認したら、vite.config.jsの

export default defineConfig({  
    plugins: [  
        laravel({  
            input: [  
-                'resources/js/app.js',
+                'resources/js/app.tsx',

にする。

ZiggyではなくWayfinderを利用できるようにする

この記事を書き始めた頃はLaravelのルート解決をJSで行うためのツールにZiggyを使ってたんだけど、この文章を書いているころにはWayfinderなるものが台頭してきた。

どうやらこのWayfinderはLaravel公式開発で、スターターキットもZiggy→Wayfinderへ移行されているということで、この記事でもWayfinderを利用する。

とりあえず以下でComposerからインストール

composer require laravel/wayfinder

つづいて、以下でVite用プラグインのインストール

npm i -D @laravel/vite-plugin-wayfinder

つづいて、vite.config.jsにwayfinder()を追加する。

import { wayfinder } from "@laravel/vite-plugin-wayfinder";

export default defineConfig({
    plugins: [
        wayfinder(),

つづいて、型定義ファイルを生成する

php artisan wayfinder:generate

このコマンドを実行するとresources/js/wayfinder, actions, routesというフォルダができる。

  • wayfinderフォルダにwayfinderに関する型定義を
  • actionsフォルダにコントローラーメソッドに関する型定義を
  • routesフォルダにルートに関する型定義を

勝手に実装してくれる。
これらのフォルダはビルドごとに再生成されるので、git管理しなくて良いっぽい。
そのため.gitignoreへ以下を追加。

/resources/js/actions
/resources/js/routes
/resources/js/wayfinder

Wayfinderの理解

これ、wayfinderroutesはわかるけどactionsってどういうこと? ってなるよね。
これは、従来まで利用されていたziggy.jsとwayfinderのコンセプトの違いだと思っている。

ziggyはあくまでもroute()関数をJSで利用できるようにするもので、wayfinderはLaravelエンドポイントをJSから利用できるするもの。

だから、名前付きルートは利用せず、以下のようなArticleコントローラーとルート定義があれば

// app/Http/Controllers/ArticleController.php
namespace App\Http\Controllers;
use App\Models\Article;

class ArticleController extends Controller
{
    public function show($id)
    {
        $article = Article::find($id);
        return view('detail', compact('article'));
    }
}
// routes/web.php
Route::get('/articles/{id}', [ArticleController::class, 'show'])->name('article.show');

以下のようにjsから呼び出せる

import { show } from "@/actions/App/Http/Controllers/ArticleController";

show(1); // { url: "/articles/1", method: "get" }

つまり、コントローラーベースでルートを呼び出せるという感じっぽい。

こうすることで、必要なルート情報だけをビルドすればよくなってパフォーマンスも良くなるし、予期しないルート漏洩も減らせるとのこと。

また、route()とは違う形式だけど、以下のように名前付きルートにも対応している。

import { show } from "@/routes/article";

// Named route is `article.show`...
show(1); // { url: "/articles/1", method: "get" }

Wayfinder + Dockerを使ってる場合

わたしはDockerを使ってLaravel環境作ってるんだけど、この場合npmでインストールした@laravel/vite-plugin-wayfinderは必要なかった。

いや、必要ないというより使えないという感じ。

このviteプラグインはコード変更に応じて勝手に

php artisan wayfinder:generate

をやってくれるというものなんだけど、これViteプラグインなのでわたしのDocker環境だとNodeコンテナでPHPコマンド実行しちゃうんだよね。

そのため、全く使えないので

npm uninstall @laravel/vite-plugin-wayfinder

して、php-fpmコンテナに

command: sh -c "php artisan wayfinder:generate && php-fpm"

とか書いておくのが最低限できることなのかな。

後は手作業でコマンドやっていく感じで。

ESLint + PrettierではなくBiomeを導入する

やっとReactやらInertiaやらを使えるようになったので、このESLintとPrettierを入れていこう…と思ったんだけど、ESLint+PrettierではなくBiomeにする。

まず、ESLintとPrettierについてなんだけど、ESLintが文法チェックツールで、Prettierがコード整形ツール。よくこの2つがコンビで使われているイメージ。
ただ、ESLintとPrettierは競合する部分があって調整必要だし、設定とか面倒なイメージも正直あった。

そんなとき、Youtubeの2025年版「Webアプリ作るなら技術どれにする?」というのでBiomeが紹介されていて、これでええやん! となったのでBiomeで。

Biomeの何が良さそうかというと

  1. Linterとフォーマッターを兼ねている
  2. あんまり設定ファイルいじんなくて良さそう!
  3. いっぱいプラグイン入れなくて良さそう!

というところ。
もちろん依存関係が無いやら、スピードが早いやら、もあると思うんだけど、個人的には以上の3つがアツい。

以下で導入

npm install --save-dev --save-exact @biomejs/biome

そんで、設定をいじりたい場合は

npx @biomejs/biome init

を実行する。

そのまま初期設定でも使えるとあったのでとりあえずは生成されたファイルをそのまま使う。

PhpStormと連携させる

これ、保存時に自動でBiomeのリンター&フォーマッタを実行してほしい。

わたしはPhpStorm使ってるので連携させる方法残しとく。

biome.jsonが無いとPhpStormのプラグインが反応してくれないので注意

簡単に言えば、

  1. Biomeのプラグインを入れて
  2. Biomeの設定で以下を有効にする
    • Run format on save
    • Run safe fixes on save
    • Sort import on save
  • そんで、PhpStormが実行されている環境でnpm installを実行する

これで、セーブしたときに実行されるようになったはず。

わたしはDockerでNode.js動かしてそこでVite動かしてるので、そっち側でnpm installしてたんだけど、そうするとNodeイメージのOSに合ったバイナリパッケージのみがインストールされちゃって、PhpStorm側のOSにあったものがインストールされなくなる。

そのせいで実行時エラーになるので、PhpStormを動かしているOS側でもnpm installをしとこう。

Lucide Reactを入れる

アイコンを使えるようになるやつだね。

npm install lucide-react

これでおk。

使い方的にはLucide Iconsでアイコンを選んで、カスタマイズして、jsxをコピーする。
そんで、テスト用のReactページに以下のようなペーストすればおk。

import { Heart } from "lucide-react";

<Heart size={112} color="#ff0000" strokeWidth={2.5} />

シンプルで素晴らしいね。

clsxを入れる

npm install clsx

clsxを入れることで

className={clsx(  
    "mt-4 rounded transition-all",  
    isActive ? "bg-black text-white" : "bg-white text-black",  
    isLarge ? "px-8 py-4 text-lg" : "px-4 py-2 text-base",  
)}

みたいにわかりやすくて、見易い動的CSSの管理ができる…と思って入れたんだけどLaravelスターターキットだと1,2箇所でしか使ってなかった。

まあ見易いし軽いので入れていいかなという判断で。

Headless UIを入れる

npm install @headlessui/react

ヘッドレスUIのheaddlessuiというパッケージだけとりあえず入れる。
個人的に、ヘッドレスUIくらい低レイヤーにするなら1から自分でコンポーネント作れば良さそうだなぁと思っているんだけど、使ってみないとわからないのでとりあえずこれだけ入れる感じ。

radixではなく、Headless UIを選んだ理由はTailwindCSS公式のパッケージだから。とりあえずこっちだけ使ってみて、ヘッドレスUIの素晴らしさを理解できたらradixにも手を出してみる。

Headless UIを入れることで

import { Switch } from "@headlessui/react";

<Switch  
    checked={isActive}  
    onChange={() => setIsActive(!isActive)}  
    className="group inline-flex h-6 w-11 items-center rounded-full bg-gray-200 transition data-checked:bg-blue-600"  
>  
    <span className="size-4 translate-x-1 rounded-full bg-white transition group-data-checked:translate-x-6" />  
</Switch>

のように扱えるようになった。

悪くなさそう。

optionalDependencies系を入れる

スターターキットにある

"optionalDependencies": {  
    "@rollup/rollup-linux-x64-gnu": "4.9.5",  
    "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1",  
    "lightningcss-linux-x64-gnu": "^1.29.1"  
}

の3つをとりあえず入れる。

入れる理由は、入れても損がなさそうだから。
意図が弱いけど、そんくらいでいく。

移行する

やっっっっっと。本当にやっっっっっっと環境が整ったので移行に移る。

とりあえず、以下のような順で進めていくことになるのかなぁと思ったりしている。

  1. テンプレートを整える
  2. GET系リクエストの処理と画面をInertia + Reactにする
  3. GET系以外のリクエストの処理と画面をInertia + Reactにする

こう聞くとそれなりにシンプルなんだけど、量えげつないし、完全に違う言語になるので作業量もすごいし、テストとかも変わるし、コンポーネントどう分けるかとかの課題もある。

えっぐいぞこれ。

プロジェクトの規模感的には、120弱のルート数と500弱のテストって感じ。ここから始めてどんくらいで終わるかなぁ。

InertiaとBladeは共存できるのである程度の妥協も必要かなとも思いつつの感じでやってく。