🎉
フルスタックなReact Router v7.0 アプリを Cloudflare Workers にデプロイする
React Router v7 とは
React Router v7 はreact-router-domの次期バージョンです。Remixと分岐されて開発されていたのが合流するらしいです。
現在pre版でdevブランチで開発されています。ドキュメントが更新されていっているのですがCloudflare Workersにデプロイする方法がまだ書かれていません。なので確認するついでに動作を検証してみました。
プロジェクトの作成
React Router v7 では "react-router-dom" が不要になり、すべて "react-router" からインポートできる。
❯ npx degit remix-run/react-router/templates/basic#dev my-react-router-app
cd my-react-router-app
React RouterのVite pluginを設定する
vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reactRouter(), tsconfigPaths()],
});
全体の構成
❯ tree app/
app/
├── app.css
├── entry.client.tsx
├── entry.server.tsx
├── root.tsx
├── routes
│ ├── _index.tsx
│ └── about.tsx
└── routes.ts
flatRoutes について
React Router v7 では、@react-router/fs-routes
パッケージを利用することで、ファイルの命名規則に基づいたルート設定が可能になります。
❯ npm i -D @react-router/fs-routes@pre
app/routes.ts
ファイル内で flatRoutes
関数を使用する
src/routes.ts
import { type RouteConfig } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export const routes: RouteConfig = flatRoutes();
二画面用意する
├── routes
│ ├── _index.tsx
│ └── about.tsx
src/routes/_index.tsx
import { Link, type MetaFunction } from "react-router";
export const meta: MetaFunction = () => {
return [
{ title: "Home" },
{ name: "description", content: "Welcome to our app!" },
];
};
export default function Index() {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Welcome to our app</h1>
<Link
to="/about"
className="text-blue-500 hover:text-blue-700 underline"
>
Go to About page
</Link>
</div>
</div>
);
}
ハイドレーションをテストしたいのでカウンターを作っておく。
src/routes/about.tsx
import React, { useState } from 'react';
export default function About() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<h1>About</h1>
<p>This is the About page.</p>
<button onClick={handleClick}>
クリック回数: {count}
</button>
</div>
);
}
サーバーエントリポイント(SSR)
src/entry.server.tsx
import type { AppLoadContext, EntryContext } from "react-router";
import { ServerRouter } from "react-router";
import { renderToReadableStream } from "react-dom/server";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext
) {
const body = await renderToReadableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
signal: request.signal,
onError(error: unknown) {
console.error(error);
responseStatusCode = 500;
},
}
);
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
クライントエントリポイント(ハイドレーション)
src/entry.client.tsx
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<HydratedRouter />
</StrictMode>
);
});
Cloudflare Workers へのデプロイ
❯ npm install -D @react-router/cloudflare@pre wrangler
vite.config.ts
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare";
import { reactRouter } from "@react-router/dev/vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [cloudflareDevProxy(), reactRouter(), tsconfigPaths()],
});
ビルドをCloudflare Workers形式に変換するためのファイル
functions/\[\[path\]\].ts
import { createPagesFunctionHandler } from "@react-router/cloudflare";
import * as build from "../build/server";
import type { ServerBuild } from "react-router";
export const onRequest = createPagesFunctionHandler({ build: build as ServerBuild });
❯ npm run build
> build
> react-router build
vite v5.4.9 building for production...
✓ 42 modules transformed.
build/client/.vite/manifest.json 1.29 kB │ gzip: 0.32 kB
build/client/assets/root-tmFNr8je.css 5.51 kB │ gzip: 1.64 kB
build/client/assets/with-props-BaPI_VtF.js 0.19 kB │ gzip: 0.17 kB
build/client/assets/about-BUqouOX8.js 0.35 kB │ gzip: 0.29 kB
build/client/assets/_index-BJPbown-.js 0.54 kB │ gzip: 0.34 kB
build/client/assets/root-DfM8saF3.js 0.78 kB │ gzip: 0.45 kB
build/client/assets/index-CPKbD0DZ.js 99.53 kB │ gzip: 33.12 kB
build/client/assets/entry.client-DGy0bJ3w.js 137.13 kB │ gzip: 44.37 kB
✓ built in 735ms
vite v5.4.9 building SSR bundle for production...
✓ 11 modules transformed.
build/server/.vite/manifest.json 0.23 kB
build/server/assets/server-build-tmFNr8je.css 5.51 kB
build/server/index.js 6.00 kB
✓ built in 49ms
❯ wrangler pages functions build --outdir build/worker
❯ npx wrangler dev build/worker/index.js
❯ open http://localhost:8787/
wrangler.toml
name = "my-react-router-app"
main = "./build/worker/index.js"
compatibility_date = "2024-10-18"
compatibility_flags = ["nodejs_compat"]
assets = { directory = "./build/client" }
❯ npx wrangler deploy
SSRが動いていることがわかる
おわりに
- Remixの中身だった
- TanStack, Honox等と行っていることは同じだった
- React Server Componentsの対応が進んだ頃にまた確認します
Discussion