2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React Router v7でSSR, CSR, SSGする

Last updated at Posted at 2024-12-08

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になります

react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
  ssr: false,
} satisfies Config;

ssr: falseのときはloaderが無効になりますし、prerenderを指定してもclientLoaderには関係ないので、ssr: falseでbuildするだけでSPAになるわけです

terminal
npm run build

buildディレクトリが生成されて、中にビルドされたファイルが配置されます

このディレクトリをそのままapacheなどで公開すればSPAとして動作しますが、仮サーバーで確認することもできます

terminal
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を書きます

routes/hoge.tsx
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>
}

ビルドします

terminal
npm run build

正しく作れているか確認しましょう

terminal
npx vite preview

上手く動いているようです

actionでデータに更新がかかると自動的にloaderが動いて表示が更新されます
これは便利

2-3. routingを試す

適当にルーティングを書きます

routes.ts
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を書きます

routes/hoge.tsx
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>
}

ビルドします

terminal
npm run build

ビルドするとindex.htmlだけが生成されます

正しく作れているか確認しましょう

terminal
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対象にします

react-router.config.ts (動的ルーティングがない場合)
import type { Config } from "@react-router/dev/config";

export default {
  ssr: true,
  prerender: true,
} satisfies Config;

これでビルドすればpre-renderしてくれます

terminal
npm run build

3-2. 動的ルーティングがある場合

prerender: trueは動的ルーティングするページはやってくれないので、ブログのようなCMSの場合は動的ルーティングするページのリストを作って渡してやることになります

react-router.config.ts (動的ルーティングがある場合)
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;

ビルドします

terminal
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するときもそのまま呼べる書き方になっていて最高です

Dockerfile
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"]

使うときは以下

terminal
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登場ですから、まだまだ「フロントエンド難しいよね」といわれる状態が続きそうでありますが、少なくとも書いてる我々は混乱しないようにしていきたいですね

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?