Next.js App Routerで絶望する「Hydration Error」の正体と、現場で使える解決策の徹底解説

技術ブログおよびガジェットレビューの専門編集者として、ご提示いただいたドラフトを精査・推敲いたしました。

今回の記事は、Next.js(特にApp Router)への移行期において非常にニーズの高い「Hydration Error」をテーマにしています。読者が抱く「なぜこれが起きるのか?」という根本的な疑問へのアプローチから、実務での使い分けといった実践的なアドバイスまで、非常に質の高い構成です。

以下に、専門編集者の視点で「よりプロフェッショナルで、かつ読み手の共感を得られる」ようにブラッシュアップした【完成版】を提示します。


Next.js App Routerで絶望する「Hydration Error」の正体と、現場で使える解決策の徹底解説

Next.jsを触り始めたエンジニアなら、一度はあの赤文字の警告に突き止められたことがあるはずだ。

"Text content does not match server-rendered HTML."
あるいは
"Hydration failed because the initial UI does not match what was rendered on the server."

特にApp Routerへの移行期において、この「Hydration Error」は非常に厄介な伏兵となる。原因を突き止めるまで数時間を溶かしたり、とりあえず`useEffect`の中に処理を逃がして思考停止(とりあえず動けばいいという妥協)に陥ったりする場面も少なくないだろう。

今回は、私が実務や個人開発で実際にハマった経験をもとに、なぜこのエラーが起きるのかという根本的な仕組みから、現場で使える「正攻法の解決策」までを深掘りしていく。単に「エラーメッセージを消す」ためのテクニックではなく、Next.jsのレンダリングモデルを深く理解した上で、スマートに解決するためのガイドとして構成した。


1. なぜHydrationエラーは起きるのか?(基礎知識の再整理)

まず前提として、Next.js(React)における「Hydration(ハイドレーション)」とは何を指しているのかを整理しておこう。

サーバーサイドレンダリング(SSR)や静的生成(SSG)を行う際、サーバー側ではHTMLを生成する。しかし、この時点でのHTMLはあくまで「ただの文字列」だ。ブラウザがこのHTMLを受け取った瞬間、ReactはJavaScriptを実行して、この静的なDOMにイベントリスナーを付加し、状態(State)を紐付ける。この「静的なHTMLに生命を吹き込むプロセス」をHydrationと呼ぶ。

エラーが発生する根本的な原因は、「サーバーが生成したHTML」と「クライアントで最初のレンダリングを行った際のReactの計算結果」に差分があることだ。

具体的には、以下のようなケースで発生する。

  • ブラウザ固有のAPIへのアクセス: `window`や`localStorage`などの値によって表示を分岐させている場合。
  • 日付や時刻の表示: サーバーでの実行時間(例:12:00:00.001)と、クライアントでのレンダリング時間(例:12:00:00.500)のわずかなズレ。
  • ランダムな数値の生成: `Math.random()`の結果がサーバーとクライアントで異なる場合。
これらは「サーバーではAと表示したのに、ブラウザで動かそうとしたらBになっちゃったじゃん!」という不整合をReactが検知し、警告(あるいはクラッシュ)を出す仕組みなのだ。

2. よくある「ハマりパターン」の比較

現場でよく遭遇する典型的なケースと、それぞれの原因・リスクを整理した。このあたりを把握しておくだけで、デバッグのスピードは劇的に上がるはずだ。

パターン原因リスク推奨される対応
`window`や`localStorage`による条件分岐クライアント固有の情報(テーマ設定等)をサーバーが知らないため発生開発環境では動くが、ビルド後の本番環境でエラーになる`useEffect`内での処理、または`dynamic`のno SSR
日時・カウントダウンの表示サーバーとクライアントのタイムスタンプの不一致微細なズレでもHydration Errorとして検知されるクライアントのみで実行するコンポーネントへの分離
`Math.random()`によるID生成など生成アルゴリズムの非決定性(Non-deterministic)コンポーネント間のID重複や不整合の原因にもなり得るUUID等の固定値をサーバー側で生成するか、CSS/JSで制御

3. 実践的な解決策:どれを使うべきか?

では、これらの問題にどう向き合うべきか。状況に応じていくつかのアプローチがあるが、それぞれに「使いどころ」の良し悪しがある。

A. `useEffect` を使った遅延レンダリング(最も一般的)

クライアントでのみ実行される処理を、マウント後に実行するように強制する方法だ。

"use client";

import { useState, useEffect } from 'react';

export default function ClientOnlyComponent() {
  const [hasMounted, setHasMounted] = useState(false);

  // マウントされたらtrueにする
  useEffect(() => {
    setHasMounted(true);
  }, []);

  if (!hasMounted) {
    // サーバーサイドとクライアントの初期レンダリングで一致する内容を返す
    // または、スケルテン(骨組み)を表示する
    return <div className="skeleton">Loading...</div>;
  }

  // ここから下は確実にクライアント側でのみ実行される
  const theme = localStorage.getItem('theme'); 
  return <div>Current Theme: {theme}</div>;
}

この手法のメリットは、「確実にエラーを回避できる」ことだ。しかし、デメリットとして、マウントの瞬間にコンテンツがチラつく(Layout Shiftに近い挙動)可能性がある点に注意が必要である。

B. `next/dynamic` を使ったSSR無効化

特定のコンポーネント自体を「サーバーではレンダリングしない」と宣言する方法だ。

import dynamic from 'next/dynamic';

// SSRを無効化した状態で動的にインポートする
const DynamicComponent = dynamic(
  () => import('../components/HeavyClientComponent'),
  { ssr: false }
);

export default function Page() {
  return (
    <div>
      <h1>Welcome</h1>
      <DynamicComponent />
    </div>
  );
}

これは、`window`オブジェクトへの依存が強いライブラリ(例:Chart.jsや地図系ライブラリなど)を扱う際に非常に有効な手段だ。コンポーネント全体を「クライアント専用」として隔離できるため、構造がクリーンに保てる。

C. `suppressHydrationWarning` 属性の付与

「どうしてもここだけはズレるかもしれないけど、仕様として許容する」というケースで使う最終手段だ。

// 日時表示など、微細な差異を許容する場合
<span suppressHydrationWarning>
  {new Date().toLocaleTimeString()}
</span>

これはあくまで「警告を抑制するための回避策」であり、根本的な解決ではない。しかし、意図的にあえてズレを許容する設計(例:現在時刻の表示など)であれば、コードを簡潔に保つための有効な選択肢となる。


4. 【深掘り】なぜ最近より目立つようになったのか?

「以前のNext.js(Pages Router)ではあまり苦労しなかったのに、App RouterになってからHydrationエラーが頻発する」と感じるなら、それは君の感覚が正しい。これにはReact Server Components (RSC) の導入による構造的な変化がある。

Pages Router時代は、基本的に「クライアントサイドのReact」をベースにSSRを組み込んでいた。一方、App Routerでは「サーバーコンポーネント」と「クライアントコンポーネント」の境界がより厳格に定義された。

特に影響しているのは以下の要因だ:

1. Streaming Rendering: App Routerでは、サーバーからコンテンツをストリームで送るため、ブラウザ側での再構築プロセス(Hydration)がよりシビアに管理されるようになった。
2. Request APIsの制約: `cookies()` や `headers()` を使う際、これらはクライアントサイドのコンポーネント内では直接アクセスできない。この設計上の制約があるため、かつて「とりあえずクライアント側で処理すればいい」と思っていた箇所を、より高度な設計(サーバーでのデータ解決)として向き合う必要が出てきた。

解決のための戦略的思考

現代のNext.js開発において推奨されるマインドセットは、「可能な限りServer Componentの範囲内でデータを完結させること」だ。

もしあるコンポーネントがHydrationエラーを起こしているなら、それは「そのコンポーネントをクライアントサイドで処理しようとしすぎている」という設計上のアラートかもしれない。
例えば、ユーザー固有のプロファイル情報を表示する際に `useEffect` で取得するのではなく、Server Componentで `fetch` を完了させてからクライアントに渡す。そうすれば、最初からサーバーとクライアントでデータが一致するため、Hydrationの問題そのものを構造的に回避できる。


5. まとめ:現場での最適解を見極める

結局のところ、どれが良いか?という問いに対する答えは「コンポーネントの責務(Role)によって使い分けること」だ。

1. 基本戦略: データ取得や静的な表示は、可能な限り Server Component で完結させる。
2. インタラクティブな要素: ボタンやフォームなど、ユーザー操作に反応する部分は `use client` を使用しつつ、Hydrationの懸念がある箇所(ローカルストレージ参照等)は `useEffect` 内でガードをかける。
3. 外部ライブラリの導入: ブラウザ専用APIを叩くものや特殊なUIライブラリには `next/dynamic({ ssr: false })` を適用する。

技術的なハマりポイントを解決するコツは、単に「エラーメッセージを消すためのコードを書くこと」ではなく、「なぜそのエラーが出ているのかというシステム側の意図を理解すること」にある。Next.jsのレンダリングモデルを正しく理解していれば、場当たり的なハックに頼らず、エレガントな実装を選択できるようになるはずだ。


🛠 実装時に役立つチェックリスト

  • [ ] `window` や `localStorage` を使う前に、その値が必要なのは本当にクライアント側だけか?(Server Componentで解決できないか?)
  • [ ] 日付やランダムなIDを生成している箇所にHydrationの懸念はないか?
  • [ ] 複雑なUIライブラリを入れる際、SSR無効化の検討を行ったか?
  • [ ] `use client` を使う範囲を必要最小限に抑えられているか?

関連する技術スタックの比較(まとめ)

技術・手法推奨されるケースメリットデメリット
Server Componentsデータ取得、静的コンテンツ高速な初期表示、バンドルサイズの削減クライアント状態の操作に制限がある
useEffectによるガードユーザー設定(テーマ等)への即時反応実装が簡単で確実にエラーを防げるレンダリング時のわずかなチラつきが発生
dynamic(ssr: false)ブラウザ専用ライブラリ、複雑なグラフライブラリの仕様を壊さずに導入できるコンポーネントの初期読み込みに影響が出る可能性がある