💨

React, Express でSSRを実現する

2023/01/21に公開

CSR(Client Side Rendering) と SSR (Server Side Rendering)

Create React App を使って作成されるアプリケーションは CSR(Client Side Rendering)で描画されます。CSR の場合、ブラウザは空の HTML をロードした後 JavaScript ファイルをロードしてコンポーネントを描画します。一方 SSR では、最初にサーバーサイド側で静的なページとして HTML を描画し、動的な JavaScript を後から注入します(hydration)。先に静的なページが表示されるため、初期描画の速度が上がります。

React における Server Side Rendering

ReactDOMServer.renderToString() というメソッドを利用することで、React Component をサーバー上で HTML として扱うことができ、hydrateRoot を利用することでその HTML に JavaScript をアタッチしてインタラクティブな動作を実現できます。

処理手順としは以下のようになります。

  1. サーバーサイドで構築された HTML を静的なページとしてブラウザに描画する。
  2. ブラウザは動的なページを構築するために必要な JavaScript をダウンロードする。
  3. コンポーネントの JavaScript がロードされた時点で、React は静的なページを動的なページに入れ替える。この時 1 で描画された DOM を再利用しつつ、イベントハンドラのみを設定する。
  4. イベントハンドラが設定され、最終的にユーザーはコンポーネントを操作できるようになる。

具体的な実装

具体的な実装を見ていきます。
リポジトリは以下にあります。

https://github.com/Kazuhiro-Mimaki/ssr-with-react-express

まずは最終的に描画したい簡単なコンポーネントを用意します。

src/App.jsx
import React, { useEffect, useState } from "react";

export const App = () => {
  const [clientMessage, setClientMessage] = useState("");
  const [count, setCount] = useState(0);

  useEffect(() => {
    setClientMessage("Hello World");
  }, []);

  return (
    <>
      <h1>{clientMessage}</h1>
      <h2>{count}</h2>
      <button onClick={() => setCount((prev) => prev + 1)}>+Click</button>
    </>
  );
};

export default App;

次に、hydrate を行います。
React18 以降ではhydrateRoot を使えば、hydrate を行うことができます。
これにより、サーバーで描画されたただの HTML がインタラクティブなものになります。

src/index.jsx
import React from "react";
import { hydrateRoot } from "react-dom/client";
import App from "./App";

const container = document.getElementById("root");
hydrateRoot(container, <App />);

最後に、サーバーの処理です。
サーバーでは、初期に描画させたいコンポーネントを HTML として用意し、レスポンスします。

server/index.js
import express from "express";
import React from "react";
import ReactDOMServer from "react-dom/server";
import App from "../src/App";

const PORT = process.env.PORT || 4000;
const app = express();

app.get("/", (req, res) => {
  // AppコンポーネントをHTML文字列に変換
  const app = ReactDOMServer.renderToString(<App />);

  // HTMLに変換されたAppコンポーネントを埋め込んだHTMLを作成
  const html = `
        <html lang="en">
        <head>
            <script src="client.js" async defer></script>
        </head>
        <body>
            <div id="root">${app}</div>
        </body>
        </html>
    `;

  // コンポーネントが埋め込まれたHTMLをレスポンス
  res.send(html);
});

app.use(express.static("./build"));

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});

動作確認

npm run start

を実行して、http://localhost:4000 にアクセスします。
Hello World という文字列と count up できる数字が表示されていれば成功です。

参考

Discussion