🤔

【Next.js】ハイドレーション & プリレンダリング / 簡単な例、考察を添えて。

2023/09/03に公開

Next.jsで以下のページを表示するとします。

import { useState } from "react";

function Hello() {
  const [count, setCount] = useState(0);
  return (
    <>
      <div>{count}</div>
      <button onClick={() => setCount(() => count + 1)}>click here!</button>
    </>
  );
}

export default Hello;

Chromeのネットワークタブを確認します。

レスポンスのHTMLは、以下のようなページを構成しています。

例えば通常のreactアプリケーションにアクセスすると、
以下のHTML要素が返ってきます。返した後に、のdivの要素内にコンポーネント等の要素が追加されていく流れになります。

省略
<div id="root"></div>
省略

では同じSPAアプリケーションなのになぜnext.jsは
HTML要素が追加された状態のHTMLをレスポンスを返すのか。

理由は、nextjsはページをプリレンダリングしておくからです。

プリレンダリングとは、事前にサーバーでHTMLを生成することを意味します。

つまり、next.jsは事前にサーバーで生成したHTMLをクライアントサイドに渡して、

ページが表示されていることになります。

しかし、
プリレンダリングで生成したHTMLファイルのボタンを押しても、
何も反応しませんが、画面上(クライアント側)にレンダリングされている画面のボタンは反応して、countが増えていきます。

なぜか?

それはハイドレーションを行なった後ものが画面上に表示されているからです。

ハイドレーションとは

生成されたそれぞれの HTML には、そのページの生成に最低限必要な JavaScript コードが関連事項づけられています。 ページがブラウザから読み込まれると、JavaScript コードが走りページをインタラクティブなものにします。(この処理はハイドレーションと呼ばれています。)

https://nextjs-ja-translation-docs.vercel.app/docs/basic-features/pages#プリレンダリング

インタラクティブとは

「対話」または「双方向」といった意味で、ユーザーがパソコンの画面を見ながら、対話をするような形式で操作する形態を指す。

https://kotobank.jp/word/インタラクティブ-901

ex)ボタンをクリック ⇄ カウント + 1

つまりブラウザでハイドレーションが行われ、onClickというイベントが動作するようになります。

またハイドレーションによってHTMLをインタラクティブなものにするだけではなく、

サーバーサイドでレンダリングされたコンテンツと、クライアントサイドでレンダリングされたコンテンツが同一か確認します。

hydrateRoot() expects the rendered content to be identical with the server-rendered content. You should treat mismatches as bugs and fix them.

https://react.dev/reference/react-dom/client/hydrateRoot#caveats

同一ではない場合どうなるか?

function Hello() {
  const num = Math.random();
  return (
    <>
      <div>{num}</div>
    </>
  );
}

export default Hello;

例えば上記のコードはMath.randomがランダムな0〜1の数を返すので、サーバー側とクライアント側で、
異なる結果がレンダリングされることが予想できます。

実際にページにアクセスしてみると以下のようなwarningがコンソールに。

Warning: Text content did not match. Server: "0.4824773639967108" Client: "0.9990220345746903"

①サーバーでは0.4824773639967108
②クライアントでは0.9990220345746903

③そして実際に画面には0.14113392596436847が表示されています。
またサーバーの0.4824773639967108 ①は一瞬画面に表示されています。

https://qiita.com/Yametaro/items/22cde58cd6abf577f1a4#参考文献

上記記事の用語を引用させていただくと

サーバから来たHTMLをもとに作られた実DOM が ①
クライアント側でReactが生成した仮想DOM が② ※画面にレンダリングされていない

結果が一致する場合はクライアントサイドでのレンダリングをスキップ、不一致の場合はクライアントサイドで再度レンダリングをします。

https://nishinatoshiharu.com/next-hydration-warning-resolution/

また上記の記事より
①と②が不一致のため、再レンダリングされ3つの数字が生まれた。。。

と考えました。

上記流れはreactのドキュメントで言及してくれています。

This is important for the user experience. The user will spend some time looking at the server-generated HTML before your JavaScript code loads. Server rendering creates an illusion that the app loads faster by showing the HTML snapshot of its output. Suddenly showing different content breaks that illusion. This is why the server render output must match the initial render output on the client.

https://react.dev/reference/react-dom/client/hydrateRoot#hydrating-server-rendered-html

最後に

ランダムな数値を返す関数によって、不一致が起こる例を書きましたが、
ブラウザ専用APIをロジックに含ませたことが原因でハイドレーションエラーが起こることもあります。

例えばcookieを使用して、コンポーネントの表示の有無のロジックを書いていた場合、
プリレンダリング時はcookieが取得できないので、クライアント側が想定する結果と不一致になるケースがあります。
対策として、getServerSideProps内でnookiesといったライブラリを使用することや、
https://www.npmjs.com/package/nookies
useEffectを使いマウント後にcookieを取得するようにすることが挙げられます。

Discussion