【Next.js】Hydration時にReact.hydrate()による警告が発生するケースとその解決方法

JavaScript

Hydrationについて

HydrationとはHTMLに付随したJavaScriptを利用してイベントリスナを登録することでインタラクティブ(操作可能)なページを生成する過程のことをいいます。Hydrationはクライアントサイド(ブラウザ)で実行されます。

Hydrationの定義と役割については、Next.js『Pre-rendering and Data Fetching#Pre-rendering』に記載されている説明と図も参照してください。

Next.jsの公式ドキュメントでも説明されている通り、HydrationというプロセスはPre-renderingの有無に関わらず実行されます。

Next.js(Pre-rendering)におけるHydrationの過程について

Next.js(Pre-rendering)のHydrationではページをインタラクティブにする処理に加えて、サーバサイドとクライアントサイドそれぞれでのレンダリング結果の比較も行われます。

クライアントサイドのレンダリングはReactDOM.hydrate()というメソッドで実行されます。
ReactDOM.hydrate()ReactDOMServer、つまりサーバサイドで作成されたHTMLをクライアントサイドで再利用するメソッドです。

ReactDOM.hydrate()サーバサイドとクライアントサイドのレンダリング結果が一致することを期待します。結果が一致する場合はクライアントサイドでのレンダリングをスキップ、不一致の場合はクライアントサイドで再度レンダリングをします。

ReactDOM.hydrate()が実行される流れの詳細解説は【図解】Next.jsのSSRが画面反映されるまでの具体的な流れで紹介しています。

ReactDOM.hydrate()による警告について

レンダリング結果の不一致はパフォーマンス低下やデザイン崩れの原因となります。

ReactDOM.hydrate()はサーバサイドとクライアントサイドのレンダリング結果が一致していることを期待しているため、レンダリング結果が一致しない場合は警告をします。

差分の内容に応じて警告メッセージは変わります。以下は警告メッセージの一例です。

レンダリング結果が異なる場合に警告されるメッセージの例
  • Warning: xxx content did not match. Server: xxx Client: xxx
  • Warning: Did not expect server HTML to contain xxx
  • Warning: Prop xxx did not match xxx
  • Warning: Did not expect server HTML to contain xxx
  • Warning: Expected server HTML to contain a matching xxx

レンダリング結果の不一致によりReactDOM.hydrate()が警告をする例

以下のコードはサーバサイドとクライアントサイドでレンダリング結果が異なる例です。
typeof window !== undefinedはサーバサイドではfalse、クライアントサイドではtrueとなります。
以下のコードでは当該条件式をレンダリング時に呼び出しているため、サーバサイドとクライアントサイドそれぞれで構築されるTSX(JSX)に差が生じます。

検証環境のnextは11.1.2を利用しています。

import type { NextPage } from "next";

const Home: NextPage = () => {
  const message = () => {
    if (typeof window !== "undefined") {
      return <div>This is Client-side</div>;
    } else {
      return <div>This is Server-side</div>;
    }
  };

  return <>{message()}</>;
};

export default Home;

以下の画像をみて分かる通り、サーバサイドからのHTMLレスポンスでは当該要素の文字列はThis is Server-sideとなっているのに対し、ブラウザではThis is Client-sideと表示されています。これはReactDOM.hydrate()によって再レンダリングされたことを意味します。

このときコンソールにはWarning: Text content did not match.から始まる警告が表示されます。

react-dom.development.js:67
Warning: Text content did not match. Server: "This is Server-side" Client: "This is Client-side"
    at div
    at Home
    at MyApp (webpack-internal:///./src/pages/_app.tsx:19:24)
    at ErrorBoundary (webpack-internal:///./node_modules/@next/react-dev-overlay/lib/internal/ErrorBoundary.js:26:47)
    at ReactDevOverlay (webpack-internal:///./node_modules/@next/react-dev-overlay/lib/internal/ReactDevOverlay.js:86:23)
    at Container (webpack-internal:///./node_modules/next/dist/client/index.js:258:5)
    at AppContainer (webpack-internal:///./node_modules/next/dist/client/index.js:754:24)
    at Root (webpack-internal:///./node_modules/next/dist/client/index.js:893:25)

サーバサイドとクライアントサイドのレンダリング結果の不一致を解消する方法

例えば副作用を活用することでサーバサイドとクライアントサイドのレンダリング結果の不一致を解消できます。
初期表示ではサーバサイドのレンダリング結果を採用し、マウント後に実行される副作用でクライアントサイドのレンダリング結果を反映させます。

具体例は以下の通りです。

import type { NextPage } from "next";
import { useEffect, useState } from "react";

const Home: NextPage = () => {
  const [isClient, setIsClient] = useState(false);
  const message = () => {
    if (isClient) {
      return <div>This is Client-side</div>;
    } else {
      return <div>This is Server-side</div>;
    }
  };

  useEffect(() => {
    setIsClient(true);
  }, []);

  return <>{message()}</>;
};

export default Home;

なお、Cookieを利用した例については【Next.js】Warningが発生する誤ったCookieの使用例と改善方法で紹介していますので、こちらもあわせてご覧になってください。

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料