laravel×Inertia.js×React×ViteでSSR
背景
laravel×Inertia.js×Reactで、Webアプリを実装しているが、
SEO対策のために、SSRをしたかった。
公式ドキュメントがあるため、
これの通りに進めたが、上手くいかずかなり詰まったので、方法をメモとして残す。
環境
laravel ^10.0
Inertia ^1.0.0
vite ^5.0.12
react ^18.2.0
手順
下準備
とりあえず、公式のドキュメント通りに進める。
ただし、既にアプリはかなり開発していたので、
php artisan breeze:install react --ssr
はスキップ。
※最後にも書いているが、開発開始時から↑で作り始めた方が楽。
次に、一応、言われた通り、
composer require inertiajs/inertia-laravel
で、Laravel adapter?を最新化。
ssr.tsxを作成
ドキュメントに従って、
touch resources/js/ssr.js
・・・。
このまま進めるとビルドでエラーになった。
touch resources/js/ssr.jsx
で、jsxファイルにする。
こうしないと、
renderで
return (
<App {...props} />
);
のところでエラーになる。
typescript化
今回はtsxにしたいので、ssr.tsxにファイル名を変える。
このとき、typescriptにいろいろ文句を言われる。
試しに、別のプロジェクトをつくって、
php artisan breeze:install react --ssr --typescript
で、スターターキットを使ってみると、typescriptによるエラーは出なかったので、
それを生成されたファイルを真似て、下記の3点の修正した。
1. package.jsonで、各パッケージを更新
パッケージが古いと、型定義ファイルがなかったりしてエラーが出るっぽい。
下記のバージョン指定で筆者はできたので参考までに。
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && vite build --ssr",
// ↑これは、後述
"postinstall": "npm run build",
},
"devDependencies": {
"@types/node": "^20.11.5",
"@types/react": "^18.0.30",
"@types/react-dom": "^18.0.11",
"@types/ziggy-js": "^1.8.0",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^6.6.0",
"typescript": "^5.0.2",
},
"dependencies": {
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.4.2",
"@inertiajs/react": "^1.0.0",
"@tailwindcss/forms": "^0.5.3",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.12",
"axios": "^1.4.0",
"laravel-vite-plugin": "^1.0.0",
"postcss": "^8.4.18",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.3.2",
"vite": "^5.0.12",
"ziggy-js": "^1.8.1"
}
}
2. 型定義ファイルを作成
js配下のTypesフォルダへ、
- global.d.ts
import { AxiosInstance } from "axios";
import ziggyRoute from "ziggy-js";
//ziggy-jsが^2.04の場合(Laravel11のスターターキットを使うとそうなるはず)、
//import { route as ziggyRoute } from "ziggy-js";
//にする必要があるかも。
declare global {
interface Window {
axios: AxiosInstance;
}
// eslint-disable-next-line no-var
var route: typeof ziggyRoute;
}
- index.d.ts
import { Config } from "ziggy-js";
export interface User {
id: number;
name: string;
email: string;
email_verified_at: string;
}
export type PageProps<
T extends Record<string, unknown> = Record<string, unknown>,
> = T & {
auth: {
user: User;
};
ziggy: Config & { location: string };
};
- vite-env.ts
/// <reference types="vite/client" />
の3ファイルを作成。
3. tsconfig.jsonを修正
{
"compilerOptions": {
"allowJs": true,
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"isolatedModules": true,
"target": "ESNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"paths": {
"@/*": ["./resources/js/*"],
"ziggy-js": ["./vendor/tightenco/ziggy"]
}
},
"include": [
"resources/js/**/*.ts",
"resources/js/**/*.tsx",
"resources/js/**/*.d.ts"
],
"exclude": ["node_modules", "vendor"]
}
ssr.tsxの中身の編集
次に、公式の通り、
ssr.tsxを編集。
import { createInertiaApp } from "@inertiajs/react";
import createServer from "@inertiajs/react/server";
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
import ReactDOMServer from "react-dom/server";
import route, { RouteName } from "ziggy-js";
//ziggy-jsが^2.04の場合(Laravel11のスターターキットを使うとそうなるはず)、
//import { route, RouteName } from "ziggy-js";
//にする必要があるかも。
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
createServer((page) =>
createInertiaApp({
page,
render: ReactDOMServer.renderToString,
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.tsx`,
import.meta.glob("./Pages/**/*.tsx")
),
setup: ({ App, props }) => {
global.route<RouteName> = (name, params, absolute) =>
route(name, params, absolute, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
...page.props.ziggy,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
location: new URL(page.props.ziggy.location),
});
return (
<App {...props} />
);
},
})
);
ポイント①
自分の場合、Contextも使っていたので、
<App {...props} />は、そのContextで実際は囲った。
ポイント②
setupの中は、
global.route<RouteName> = (name, params, absolute) =>
route(name, params, absolute, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
...page.props.ziggy,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
location: new URL(page.props.ziggy.location),
});
を追加している。
これがないと、ビルド後に、routeがないぞ!みたいなエラーが出た。
route内は、typescriptのエラーが出てしまうが、スターターキットでも、
// @ts-expect-error
と書かれていてエラーが出るもののようなので、無理矢理消している。
ポイント③
titleで、app.tsxのように、
const appName =
window.document.getElementsByTagName("title")[0]?.innerText || "Laravel";
を使っていると、windowがないぞと言われるので、
.envに、
VITE_APP_NAME="アプリ名"
を追加して、
const appName =
import.meta.env.VITE_APP_NAME || "Laravel";
に変える。
ポイント④
スターターキットでは、
import route from "../../vendor/tightenco/ziggy/dist/index.m";
のように、routeを型定義しているが、
そのようにして、Herokuにデプロイしたらビルド時にエラーが出た。
(なぜか、ローカルで、npm run buildをしても出なかった。)
なので、
import route, { RouteName } from "ziggy-js";
のようにrouteを定義したらうまくいった。
vite.config.jsの修正
次に、
vite.config.jsを編集。
import react from "@vitejs/plugin-react";
import laravel from "laravel-vite-plugin";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
laravel({
input: "resources/js/app.tsx",
ssr: "resources/js/ssr.tsx",
refresh: true,
}),
react(),
],
ssr: {
noExternal: [
"@mui/material",
"@mui/utils",
"@mui/base",
"@mui/icons-material",
],
},
});
ポイント
ssr: {
noExternal: [
"@mui/material",
"@mui/utils",
"@mui/base",
"@mui/icons-material",
],
},
ここ。
これを書いておかないと、ビルドは成功しても、アクセスしたときに、
Element type is invalid
みたいなエラーが出て、画面は真っ白になる。(npm run devの場合は、エラーにならない)
これが原因がわからず、かなり迷走した。
最初のbreezeのスターターキットだと、このエラーは出ないのに、開発中の自分のアプリだとエラーになり続けた。。
自分の開こうとしているコンポーネントの問題だろうと考えて、
ssr.tsxに空の状態から、順に元のコンポーネントの要素を加えていったところ、
MaterialIconのとあるアイコンを含むコードを入れた瞬間、このエラーが出始めること
がわかった。
いろいろ調べた結果、
が症状として近そうだったので、これを参考にしたら上手くいった。
※この記事だと、noExternalを上記のようにすると、yarn devが上手くいかなかったらしいが、自分はnpm run devも問題なくできていた。
buildコマンドを追記
package.jsonを、
build": "vite build && vite build --ssr
に書き換え。
npm run build
でビルドが成功するはず。
成功したら、
php artisan serve
とあわせて、
php artisan inertia:start-ssr
を行う。
で、ブラウザでアプリを開くが、このとき、ブラウザのjavascriptをオフにしてみる。
Chromeの場合、URLバーの左側のカギマークから「サイトの設定」でオフにできる。
うまくいっていれば、これをしてもページがちゃんと開ける。
ハイドレーションを適用
app.tsxを下記のように編集。
import "./bootstrap";
import "../css/app.css";
import { createInertiaApp } from "@inertiajs/react";
import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
import { hydrateRoot } from "react-dom/client";
const appName =
window.document.getElementsByTagName("title")[0]?.innerText || "Laravel";
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.tsx`,
import.meta.glob("./Pages/**/*.tsx")
),
setup({ el, App, props }) {
hydrateRoot(
el,
<App {...props} />
);
},
progress: {
color: "#4B5563",
},
});
これで本番環境は問題ないが、 npm run devをしたとき、ブラウザのconsoleに
Expected server HTML to contain a matching <div> in <div> .
のような警告が出る。
buildではでなかったが、原因がわからず。
無視するのも気分が悪いので、開発環境では、通常のcreateRootになるように、
if (import.meta.env.VITE_APP_ENV === "production") {
hydrateRoot(
el,
<App {...props} />
);
} else {
createRoot(el).render(
<App {...props} />
);
}
のように無理矢理分岐した。
buildで生成されるファイルをgit管理対象外に
このままだと、npm run build時に生成されたファイルがgitで管理されてしまうので、
プロジェクト直下の .gitignoreに、
/bootstrap/ssr
を追加して、git管理対象から外しておく。
Procifileの修正(Herokuデプロイ用)
Herokuにデプロイする場合は、
Procfileを
web: php artisan inertia:start-ssr & vendor/bin/heroku-php-apache2 public/
下記のようにする。
かなり、難航したので、書き残しておきました。
本当は、laravel breezeを使っているので、
php artisan breeze:install react --ssr --typescript
をして、開発を始めていたら、ここまで難航しなかったと思う。。
公式のドキュメントに書かれていない、setup内に記載するrouteのところなんかも、breezeのスターターキットでファイルを作成すると書かれていたりした。
まぁ、なんにせよ、フロントエンドの技術としてviteなんかの勉強にはなったかな・・・
※追記
別アプリで、
react-material-ui-carousel
を使っているが、そこでSSRのエラーが出てしまう。(ユーザーの画面上はでないが)
上述のmaterial iconsと同様に、
ssr: {
noExternal: [
"@mui/material",
"@mui/utils",
"@mui/base",
"@mui/icons-material",
],
},
ここに加えればいいかと思ったが、追加しても、別のエラーで上手くいかない。
今回は、そのカルーセル自体を残すか検討中だったため、その場しのぎとして、
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
{isClient && (
<Carousel ...中略... />
)}
をカルーセルを使っているコンポーネントに追記し、無理矢理SSRでカルーセルを表示しないようにした。
正しい解決策をご存じの方いたら、ご教示ください・・・。。
Discussion