1. 前提や情報整理
1-1. やりたいこと
チュートリアルをざっとやったので次はビルド~デプロイを学びたい
1-2. SSR, CSR, SSG
最近のフルスタックフレームワークではSSR, CSR, SSGの使い分けを考慮すると思います
これらはビルドやデプロイのやり方が違いますし、コードや設定ファイルでフレームワークに上記を指示するので、これらをよく理解しておく必要があります
種類 | 略 | 説明 |
---|---|---|
Client Side Rendering | CSR | ブラウザでJavascriptがレンダリングを |
Server Side Rendering | SSR | サーバーがhtmlをレンダリングする |
Static Side Generation | SSG | build時にhtmlをレンダリングする |
これらを組み合わせて使うことができるのでより一層ややこしく感じますが、できるデプロイ方法は以下の2つが基本になります
デプロイ方法 | 例 | SSR | CSR | prerender |
---|---|---|---|---|
専用サーバー | npm run startなど | 可能 | 可能 | 可能 |
htmlサーバー | apacheなど | できない | 可能 | 可能 |
SSGと言ってしまうとフルにpre-renderingしてhtmlサーバーなりCDNなりで配信する方法全体を指しているように思えるので、ここからはビルド時にhtmlにレンダリングする、という意味をprerenderと表現していきます
1-3. React Router v7ではどう書くか
指示に対する処理は以下のようになります
・SSRしたい情報はloader、CSRしたい情報はclientLoaderで書く
・prerender指定されたページはloaderがprerenderされる
・ssr: falseではloaderが処理できない (エラーになる)
意外と簡単です
フェッチ方法 | prerender | 扱い |
---|---|---|
clientLoader | 有効 | CSR |
clientLoader | 無効 | CSR |
loader | 有効 | prerender |
loader | 無効 | SSR |
1-4. SSGはできる?
フェッチをloaderでやっておいてすべてのページをprerenderするようにしたらSSGできてしまいます。これが公式にできるようにしているのか、たまたま出来るようになってしまっているのかは分からないので注意が必要です
実際やってみたのが以下
ssr: trueなのでserverディレクトリも作られてしまっていますが、build/clientの中身だけで完結しているので、build/clientだけapacheのドキュメントルートに置いてやれば普通に正しく表示されます
詳しいやり方と懸念については後で解説します
2. SPA
まずはSPAから見ていきたいと思います
2-1. 書き方
SPAについては公式ドキュメントにページがあります
https://reactrouter.com/how-to/spa
ssr: falseとすればSPAになります
import type { Config } from "@react-router/dev/config";
export default {
ssr: false,
} satisfies Config;
ssr: falseのときはloaderが無効になりますし、prerenderを指定してもclientLoaderには関係ないので、ssr: falseでbuildするだけでSPAになるわけです
npm run build
buildディレクトリが生成されて、中にビルドされたファイルが配置されます
このディレクトリをそのままapacheなどで公開すればSPAとして動作しますが、仮サーバーで確認することもできます
npx vite preview
http://localhost:4173 に表示されます
2-2. clientLoader, clientActionを試す
以前Remix SPAモードで趣味アプリを書いたときにloaderがちゃんと動かないので何故かと思ったら、どうもloaderはSPAモードでは使えないらしいという記載を見つけてあきらめたことがあるのですが (本当にそうかあまり自信がない)、React Router v7はclientLoaderが使えると明記しているのでちゃんと使える筈。試してみたいと思います
APIを準備
適当なAPIを準備しました
今回は練習なのでCORSは全部許可済みにしてあります
Route Moduleを準備
clientLoader、clientActionで取得/更新するようにRoute Moduleを書きます
import type { Route } from "./+types/home";
import { Form } from "react-router";
const URL = `http://localhost:8000/hoge/`
export async function clientLoader({
params,
}: Route.ClientLoaderArgs) {
const res = await fetch(URL);
const data = await res.json();
console.log('clientLoader: ', data)
return data;
}
export async function clientAction({
request,
}: Route.ClientActionArgs) {
const formData = await request.formData();
const text = formData.get("text")
console.log('clientAction: ', text)
const data = { text: text }
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
};
try {
const response = await fetch(URL, requestOptions);
if (response.ok) {
const d = await response.json();
//console.log(d);
} else {
throw new Error('Network response was not ok');
}
} catch (error) {
console.error('There was a problem with the fetch operation:', error);
}
return null;
}
export default function Home({
loaderData,
}: Route.ComponentProps) {
// return <Welcome />;
return <div style={{
padding: '50px',
fontSize: '34px'
}}>
<Form method="post">
<input type="text" name="text" style={{border: '2px solid #333'}} />
<button type="submit" style={{border: '2px solid #333', marginLeft: '20px'}}>Submit</button>
</Form>
{
loaderData.map((hoge: { id: number, create_at: Date, text: string }) =>
{ return <div key={hoge.id}>{hoge.text}</div> }
)
}
</div>
}
ビルドします
npm run build
正しく作れているか確認しましょう
npx vite preview
上手く動いているようです
actionでデータに更新がかかると自動的にloaderが動いて表示が更新されます
これは便利
2-3. routingを試す
適当にルーティングを書きます
import {
type RouteConfig,
index,
route,
} from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("/hoge", "routes/hoge.tsx"),
route("/fuga", "routes/fuga.tsx"),
route("/todos", "routes/todos.tsx"),
] satisfies RouteConfig;
適当なRoute Moduleを書きます
import type { Route } from "./+types/home";
import { Link } from "react-router";
export default function Home() {
return <div style={{
padding: '50px',
fontSize: '34px'
}}>
<Link to="/">
HOGE
</Link>
</div>
}
ビルドします
npm run build
ビルドするとindex.htmlだけが生成されます
正しく作れているか確認しましょう
npx vite preview
以下を開くと
http://localhost:4173/hoge
こんな感じ
ちゃんと出来てますね
3. SSG
pre-renderについては公式ドキュメントにページがあります
https://reactrouter.com/how-to/pre-rendering
公式ドキュメントにはSSGができると明記されていないが、ssr: trueにしてすべてのページをprerenederの対象にすれば結果としてSSGはできるようです
3-1. 動的ルーティングがない場合
loaderが使えるようにssr: trueにして、ページすべてをprerender対象にします
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
prerender: true,
} satisfies Config;
これでビルドすればpre-renderしてくれます
npm run build
3-2. 動的ルーティングがある場合
prerender: trueは動的ルーティングするページはやってくれないので、ブログのようなCMSの場合は動的ルーティングするページのリストを作って渡してやることになります
import type { Config } from "@react-router/dev/config";
const makePrerenderList = async () => {
const URL = `http://localhost:8000/hoge/`
const res = await fetch(URL);
const data = await res.json();
const toPrerender = ["/"].concat(
data.map((hoge: { id: number, create_at: Date, text: string }) => "/"+hoge.id)
);
console.log(toPrerender)
return toPrerender
}
export default {
ssr: true,
async prerender({ getStaticPaths }) {
return makePrerenderList()
}
} satisfies Config;
ビルドします
npm run build
build/clientディレクトリにSSGされた内容が書き出されています。ssr: trueなのでbuild/serverディレクトリもありますが、build/clientディレクトリの中身だけapacheで公開すると正常に動作したので問題なさそうです
3-3. 懸念
上記でビルドしてからnpx vite previewすると、SSRで公開しようとしてしまうので正常に表示されません。また、公式ドキュメントでもSSGできると書かれておらず、検索しても開発チームからそれらしき発言がありませんし、React Router v7にマージされたRemix v2側は明確にSSGに対応しない考えを表明しています。この事から、SSGできるというのは開発チーム側がそうあるべきとして作っているのでなく、たまたま出来てしまっている可能性も多分にあると思われます
「SSGできるじゃんわーい」とプロダクションに導入してしまった場合、ある日のバージョンアップでできなくなってしまうかもしれません。仕事で使うのであれば明確に対応を表明しているAstroなどを使うか、逆にCloudflare WorkersなどでSSRとして公開するのが無難だし、SEOが関係ないならシンプルにSPAで作るのが良いと思います
4. SSR
SSRしようと思うとサーバーが必要になりますが、テンプレートリポジトリがGithubに公開されていて、公開方法ごとの手順が分かるようにしてくれています
https://github.com/remix-run/react-router-templates/tree/main
ssr: trueにしておくと、CSR, SSRをclienetLoader, loaderの選択で、prerenderをそれぞれページ単位で任意に設定できます
4-1. dockerでNode.jsサーバーを立てる
手順は以下リンクにあります
https://github.com/remix-run/react-router-templates/tree/main/node-custom-server
React Router v7でプロジェクトをつくると以下のようなファイル構成になり、最初から作られている謎のDockerfileがありますが、これを使います
Dockerfileの中身を見ると、以下のようにマルチステージビルドでビルドしてからnpm run startするようになっています。docker compose upするときもそのまま呼べる書き方になっていて最高です
FROM node:20-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci
FROM node:20-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev
FROM node:20-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build
FROM node:20-alpine
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
WORKDIR /app
CMD ["npm", "run", "start"]
使うときは以下
docker build -t my-app .
docker run -p 3000:3000 my-app
4-2. それ以外の方法
ホスティングサービスなどを使った手順も以下に書かれています
https://github.com/remix-run/react-router-templates/tree/main
リポジトリにフォルダがたくさんありますので自分が使いたいデプロイ方法を選ぶとreadme.mdに使い方がめちゃめちゃ分かりやすく書いてあります
まとめ
SPA, SSG, SSRの書き方について整理してみました
SPAとSSRは公式に対応していますし、SSRを基本としてCSRとprerenderを組み合わせることもできます。SSGは多分ダメなんじゃないかと思いますが一応できますし、Astro使えばSSGできますから、loader, actionが好きならReact Router v7はかなり良いと思います
書き方が整理されていて分かりやすくなっているのも魅力で、SPA, SSRを書く際に選ばれることが増えていくのではないかと思います。React19でRCSが登場して更に複雑さを増やす方向になっていますが、Remix v3もRCSに対応してリライトされる予定であるなど各フレームワークがRCSに対応してくるでしょうし、今回整理が進んだように分かりやすさと高機能が両立するようになっていくのではないかと期待しています
レッツトライ
余談
SSRと言ったとき以下のような概念が混じっていて書きにくいと感じました
・サーバーサイドでフェッチを行うこと
・フェッチした情報をサーバーサイドでhtmlにレンダリングすること
・SSRを行うアプリケーションデプロイ方式のこと
似たようなことを感じている人もいるようです
https://zenn.dev/sumiren/articles/349c60f19c505f
こういうのを読むとフロントエンドフレームワークは新しい方式がどんどん出てきて概念整理がまだ足りない状態なのかなと思います。ルーティングのあるSPAというのも改めて考えるとトゲナシトゲトゲ的な面白呼称ですし、SSRアプリはSSRだけでなくCSRやprerenderも混在できるわけで、本当にSSRという呼び方で良いのか?というのは皆思うところだと思います
もう少ししたら整理が進んでいくのかなと思ったところでRSC登場ですから、まだまだ「フロントエンド難しいよね」といわれる状態が続きそうでありますが、少なくとも書いてる我々は混乱しないようにしていきたいですね