2025年07月25日~2025年10月12日
半年前くらいに作曲知識共有サイトをどうにか動く形にしてdtmru α版としてリリースしたんだけど、改善したいところが無限にあるので改善する。
なにをしたいのか
とりあえず、以下の2つを実行したい
- フロントをInertia.js + Reactへ
- 記事エディタを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をするために必要なフレームワーク
- ただでさえInertiaだけでも以下を学ばなきゃいけない
- LaravelがInertia推してるので
- 学習コストデカすぎなので
- 記事エディタをTipTapにしたい
- 現在のCKEditorが使いにくいから
- ヘッドレスでUIを自由自在にいじれるから
- PHP用のサニタイズライブラリがあったり、Livewire、AlpineのチュートリアルがあったりとLaravelと相性が良さげ。どっちも使わない予定だけど。
- エディタをTipTapにするついでにフロントをモダンにしたい
- SSR + SPAにはしたい
- まだわからないけど、音を扱う以上、音のプレイヤーとか実装するとなったらSPAじゃなきゃだめだし、記事共有サイトという性質上、SEOのためにSSRでありたい
その上で、取れる選択肢は以下
- Livewire へ移行
- Livewireを使うんだったらReactかVueを学びたいのでペケ
- 現行のBlade+Alpine.jsで実装
- Blade + Alpine.jsは非常にありなんだけど、ならLivewireでよくない? となるし、先のことを考えるとSSR + SPAにしたいのでペケ
- LaravelをバックエンドAPI化して、Next.jsとかをフロント側にする
- 最初はフロントとバックエンドで完全にサーバーを分けるこの形を考えていたんだけど、明らかにオーバースペックなのでペケ。あと、Next.jsを使うならもうNext.jsだけで実装しちゃいたいかも。
- Inertia + Reactへ移行
- 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つっぽい
- inertiajs/inertia-laravel
- inertiajs/inertia-laravelはInertia使うので必要
- 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
- 本体
- @eslint/js
- eslint公式のjs用ルール
- eslint-config-prettier
- prettier(後述)とルールが競合しないようにするもの
- eslint-plugin-react
- React用のルール
- eslint-plugin-react-hooks
- ReactのHookに関するルール
- typescript-eslint
- ESLintでTypeScriptを使用できるようにするもの
ここらへんは全部導入するつもりだったんだけど、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でよいので導入しない。
- prettier
- 本体
- prettier-plugin-organize-imports
- import文の自動整形を追加
- prettier-plugin-tailwindcss
- TailwindCSSのクラスを自動で並び替えてくれる。めっちゃいいね
@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デフォなので思い切って採用する。
- typescript
- 導入する
- 本体
- @types/react
- 導入する
- reactの型定義
- @types/react-dom
- 導入する
- react-domの型定義
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"
スターターキットには大きく分けて
- Headless UI
- 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のビルドで利用するモジュールバンドラー。
前述通り、別環境でもそれにあったものがインストールされるっぽいので記述しておいてもいいのかなという感じ。
- Viteのビルドで利用するモジュールバンドラー。
- @tailwindcss/oxide-linux-x64-gnu
- TailwindCSSのlinux-x64-gnu用バイナリ。Tailwind使うから指定しとこ。
- lightningcss-linux-x64-gnu
- ViteのCSS圧縮で利用するCSSパーサー。Vite使うし入れとこ。
私が導入するパッケージ
そんな感じでほとんどのパッケージは入れる感じに。
かなりカオスになりそうで心配である。
なんかこれ、マイクラのmodをとりあえずいっぱい入れる時と同じ不安を感じる。
環境のインストールや初期設定やら
いきなり全部入れるとワケワカメになるので、以下のような段階的な導入を行う。
- React+TSの導入
- Inertia.jsの導入
- ESLint + Prettierの導入
- それ以外の導入
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
でいいね。
- 出力するjsのバージョン指定。
- 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でいく。
- オプショナルなプロパティにundefinedを代入することを禁止する設定。ただ、明示的にundefinedを型宣言すれば代入できるようになる。
- jsx
- tsxをどう処理するかの設定。
5つくらい処理方法の選択肢があるんだけど、今回はReact 17以上から対応しているreact-jsx
を指定する。どうやらreactのimportが要らなくなるらしい。
- tsxをどう処理するかの設定。
- verbatimModuleSyntax
- 型専用のimport/exportと普通のimport/exportの使い分けを厳格にする。
オンでいいのでは。
- 型専用のimport/exportと普通のimport/exportの使い分けを厳格にする。
- isolatedModules
- これは、ファイル単位でのトランスパイル結果が定まるかをチェックするか否かのルール。TSはファイル全体を見るので問題ないらしいんだけど、他のトランスパイラは1ファイルずつ処理することがあるので、その場合に対応するためのルールみたい。
詳しくはQiitaの「TypeScriptを他のツールで取り扱うためのコンパイラオプションについて」がわかりやすかった。
- これは、ファイル単位でのトランスパイル結果が定まるかをチェックするか否かのルール。TSはファイル全体を見るので問題ないらしいんだけど、他のトランスパイラは1ファイルずつ処理することがあるので、その場合に対応するためのルールみたい。
- noUncheckedSideEffectImports
- 副作用的に使うimport先のソースファイルがない場合にエラーにするルール。
「副作用」っていうのは、importした値や関数を使わず、読み込むだけで発揮されるコードの効果だけを得るみたいな利用方法のこと。
これもオンでいいのでは。
- 副作用的に使うimport先のソースファイルがない場合にエラーにするルール。
- moduleDetection
- TSがファイルをモジュールまたはスクリプトと判断するルールの設定。
基本的にスクリプトとしてJSは書かないので、force
にしてモジュールに強制する感じで良さそう。
- TSがファイルをモジュールまたはスクリプトと判断するルールの設定。
- 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
なんだね
- TSがどの形式のモジュールで出力するか。ブラウザが利用するmoduleなので
- moduleResolution
- モジュールの探索アルゴリズムの指定。Viteを使う場合は
bundler
を使うみたい。なぜbundlerを利用するのかについては、「TypeScriptで”moduleResolution”: “Node”は使わないほうがいい」という記事がかなり良い。
TSConfigの設定むずいね
- モジュールの探索アルゴリズムの指定。Viteを使う場合は
- allowJs
- jsファイルをインポートできるようにする設定。段階的にJS→TSへ移行する際に使えるね。
- noEmit
- JSファイルをコンパイルして出力しないようにする設定。
Viteを使っていて、Viteがコンパイルするのでtrueかな。
- JSファイルをコンパイルして出力しないようにする設定。
- esModuleInterop
- これをtrueにするとCommonJSも
import example from 'example
;みたいにインポートできるようになって、型定義も正確になるみたい。
つまり、CommonJSを使うパッケージとかを使うならtrueにしたい感じなのかな。
デメリットはバンドルサイズが若干増えるとかぽい。
- これをtrueにするとCommonJSも
- forceConsistentCasingInFileNames
- ファイル名の大文字小文字を正確に区別するかどうかの設定。オンでいいね。
- strict
- TSの設定にはいくつかStrict系のルールがあるんだけど、それをすべてオンにするというもの。ストイックだね。
- noImplicitAny
- strict系のルールの1つ。上の
strict
を有効にするとこれは自動でtrue
になる。そのため、なぜこれだけ明示的に指定しているのかは謎。
trueにすると、型注釈がない場合にエラーになる。falseだとanyで勝手に推測してくれる。
- strict系のルールの1つ。上の
- 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>
データを送れる
- この部分にReactから
- @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の理解
これ、wayfinder
とroutes
はわかるけど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の何が良さそうかというと
- Linterとフォーマッターを兼ねている
- あんまり設定ファイルいじんなくて良さそう!
- いっぱいプラグイン入れなくて良さそう!
というところ。
もちろん依存関係が無いやら、スピードが早いやら、もあると思うんだけど、個人的には以上の3つがアツい。
以下で導入
npm install --save-dev --save-exact @biomejs/biome
そんで、設定をいじりたい場合は
npx @biomejs/biome init
を実行する。
そのまま初期設定でも使えるとあったのでとりあえずは生成されたファイルをそのまま使う。
PhpStormと連携させる
これ、保存時に自動でBiomeのリンター&フォーマッタを実行してほしい。
わたしはPhpStorm使ってるので連携させる方法残しとく。
※biome.json
が無いとPhpStormのプラグインが反応してくれないので注意
簡単に言えば、
- Biomeのプラグインを入れて
- 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つをとりあえず入れる。
入れる理由は、入れても損がなさそうだから。
意図が弱いけど、そんくらいでいく。
移行する
やっっっっっと。本当にやっっっっっっと環境が整ったので移行に移る。
とりあえず、以下のような順で進めていくことになるのかなぁと思ったりしている。
- テンプレートを整える
- GET系リクエストの処理と画面をInertia + Reactにする
- GET系以外のリクエストの処理と画面をInertia + Reactにする
こう聞くとそれなりにシンプルなんだけど、量えげつないし、完全に違う言語になるので作業量もすごいし、テストとかも変わるし、コンポーネントどう分けるかとかの課題もある。
えっぐいぞこれ。
プロジェクトの規模感的には、120弱のルート数と500弱のテストって感じ。ここから始めてどんくらいで終わるかなぁ。
InertiaとBladeは共存できるのである程度の妥協も必要かなとも思いつつの感じでやってく。