【完成版】深夜2時の絶望。Next.js App Routerのハイドレーションエラーと戦って悟ったこと

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

元の文章が持つ「深夜の熱量」や「開発者の共感」を活かしつつ、より技術解説としての正確性を高め、読者の満足度を向上させるための調整を行いました。特に、ハイドレーションエラーの本質的な原因(なぜ不一致が起きるのか)についての論理構成を強化し、読み物としての完成度を高めています。

以下に、修正・校正済みの【完成版の記事】を出力します。


【完成版】深夜2時の絶望。Next.js App Routerのハイドレーションエラーと戦って悟ったこと

深夜2時。モニターの光だけが、自分の顔を青白く照らしている。
コーヒーのカフェインも切れかけで、意識がわずかに遠のき始めている。しかし、目の前のコンソールに突きつけられた赤文字――「Hydration failed because ...」というエラーメッセージは、私の集中力を強制的に引き戻した。

個人開発やプロダクト開発を進めていると、この瞬間は何度か訪れる。
特にNext.jsのApp Routerへの移行期において、React Server Components(RSC)とClient Componentsの境界線が曖昧になりやすい今、ハイドレーションに関するトラブルは「避けて通れない関門」と言っても過言ではない。

今日は、私がこのエラーと格闘しながら学んだこと、そして同じ壁に突き当たっている開発者が明日から実践できる具体的な解決策を、ありのままの熱量を込めて書き残しておこうと思う。


なぜ「ハイドレーション(Hydration)」で躓くのか

まず、根本的な原因を整理しよう。
初心者の頃は、「サーバーでHTMLを作って、クライアントでJavaScriptを流し込むだけだろ?」と考えていた。しかし、Reactの動的なエコシステムにおいては、そのプロセスが非常に繊細な同期を要求する。

ハイドレーションとは、サーバーから送られてきた静的なHTML構造に対して、ブラウザ側でReactのイベントリスナーや状態(State)を紐付けるプロセスのことだ。

この時、以下の条件が満たされないとエラーが発生する。
「サーバーでレンダリングした際のDOM構造」と「クライアントでの初期レンダリング時のDOM構造」が一致していない場合である。

具体的には、以下のようなケースが典型的な原因となる。

  • `new Date()` や `Math.random()` をコンポーネント内で直接使用している。

  • `window` や `localStorage` といったブラウザ固有のAPIを、クライアント側での実行を待たずに参照している。

  • ノードの存在有無(例:特定のプラグインが挿入した要素)によってDOM構造が変わる処理が含まれている。


これらは「サーバーではAというHTMLを生成したが、クライアントでReactが起動した瞬間にBと判断した」という不一致を生む。このわずかなズレが、ブラウザ上での描画の乱れや、最悪の場合はJavaScriptの実行停止を引き起こすのだ。


レンダリング戦略の再整理

ここで一度立ち止まって、現在のNext.jsにおける主要なレンダリング手法の違いを整理しておこう。この構造を正しく理解していないと、なぜ特定のコンポーネントでエラーが出るのかの判断がつかなくなるからだ。

方式特徴主な用途メリットデメリット
SSR (Server-Side Rendering)リクエストごとにサーバーでHTMLを生成動的なコンテンツ(ニュース等)SEOに強く、初期表示が早いサーバー負荷が高まる
SSG (Static Site Generation)ビルド時に静的HTMLを生成ブログ、ドキュメントなど爆速な配信と高いキャッシュ効率更新のたびに再ビルドが必要
ISR (Incremental Static Regeneration)定期的にバックグラウンドで再生成商品ページ(頻繁な更新がある)静的な高速さと動的な更新を両立反映にタイムラグがある
CSR (Client-Side Rendering)ブラウザ上でJSを実行して描画ダッシュボード、設定画面などユーザー操作への反応が良い初期表示が遅く、SEOに弱い

私たちが陥っている「ハイドレーションエラー」の多くは、SSR/SSGの領域で実行されるべき処理を、クライアント側のロジックと不適切に混在させてしまったことから発生する。


実際に直面した問題:ブラウザ固有機能との衝突

今回私が数時間悩まされたのは、ユーザーの好みに合わせてテーマ(ダークモード/ライトモード)を切り替えるための「初期状態の判定」だった。

単純に `localStorage` を参照してクラスを付与しようとすると、サーバー側では `localStorage` が存在しないためデフォルト値が選ばれ、クライアント側で読み込んだ瞬間に値が変わる。これがハイドレーションを混乱させる典型的なパターンだ。

失敗するコードの例

// 【NG】Hydration Errorの原因になる例
import { useState } from 'react';

export default function ThemeToggle() {
  // サーバー側では localStorage がアクセスできないため、
  // ここで undefined やデフォルト値が入り、クライアントと不一致が起きる。
  const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light');

  return (
    <div className={theme === 'dark' ? 'bg-black' : 'bg-white'}>
      Current theme: {theme}
    </div>
  );
}

解決策1:`useEffect` を用いた遅延実行

最も標準的な解決策は、ハイドレーションが終わるまで「クライアントでの判定」を待機させることだ。

import { useState, useEffect } from 'react';

export default function ThemeToggle() {
  const [theme, setTheme] = useState<string | null>(null);

  useEffect(() => {
    // この中身はマウント後にしか実行されないため、
    // サーバーとクライアントの不一致(Hydration mismatch)を回避できる。
    const savedTheme = localStorage.getItem('theme');
    setTheme(savedTheme || 'light');
  }, []);

  if (theme === null) {
    // ハイドレーションが終わるまでの間、プレースホルダーやスケルトンを表示する
    return <div className="opacity-50">Loading...</div>;
  }

  return (
    <div className={theme === 'dark' ? 'bg-black' : 'bg-white'}>
      Current theme: {theme}
    </div>
  );
}

解決策2:`dynamic` インポートによる `ssr: false` の活用

特定のコンポーネント(例:複雑なUIライブラリや、どうしてもブラウザAPIに依存するウィジェット)をまるごと隔離する場合、Next.jsの `dynamic` を使うのが最もクリーンなアプローチだ。

import dynamic from 'next/dynamic';

// SSRを無効化してインポートすることで、サーバー側でのレンダリングをスキップさせる
const NoSSRComponent = dynamic(() => import('../components/HeavyClientComponent'), {
  ssr: false,
});

export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      {/* このコンポーネントはクライアント側でのみ実行されるため、ハイドレーションエラーを回避できる */}
      <NoSSRComponent />
    </main>
  );
}


思考の深掘り:なぜ「とりあえず動く」ではいけないのか

ここで少しだけ哲学的な話をしよう。なぜ私がこれほどまで「正しい構造」にこだわって書いているのかというと、「技術負債の蓄積」を極端に嫌うからだ。

`useEffect` で解決する手法は手軽だが、プロジェクト全体でこれを無秩序に乱用すれば、ユーザー体験(UX)が損なわれる。例えば、ハイドレーション待ちの間にコンテンツがちらつく(Layout Shift)現象が発生し、視覚的なノイズとなるからだ。

一方で、`dynamic(..., { ssr: false })` を使う場合は、SEOに影響する重要なコンテンツには使用しないというルールを自分に課す必要がある。

「どこで、どの技術を使うべきか」の判断基準を持っておくことは、開発における生存戦略に近い。

状況推奨されるアプローチ理由
フォーム入力やボタン操作Client Component + `useState`インタラクティブな要素はクライアント側で処理すべきだから
外部APIとの連携(初期表示)Server Component (RSC)サーバーから直接データを取得することで、ハイドレーションの負荷を最小限に抑えられる
ブラウザ特有機能の利用`dynamic` で SSR を無効化ブラウザ環境に依存するコードがサーバーで実行されるのを防ぐため
ユーザー設定(テーマ等)`useEffect` + 状態管理クライアント側でのみ動くロジックを、安全な境界線の中で処理するため

まとめ:深夜の静寂の中で見つけた答え

結局のところ、Next.jsやReactの最新スタックにおける開発は、「サーバーとクライアントの橋渡し」をいかにスムーズに行うかというパズルだ。

ハイドレーションエラーに遭遇したとき、それは単なるバグではない。「あなたが今、サーバーとクライアントの境界線を曖昧な場所で通ろうとしている」というシステムからの警告である。その通知を無視せず、適切にコンポーネントを分離し、適切なライフサイクルの中で処理を動かす。この積み重ねが、堅牢なアプリケーションを作る唯一の道だ。

さて、エラーも消え、ようやくコードが正常にビルドされた。
明日はもう少し早く起きて、この解決策をプロダクト全体に適用する作業に取りかかろう。

コーヒーをもう一杯淹れることにする。


💡 今回の学びのポイント

1. ハイドレーションエラーの本質: サーバーとクライアントでの「最初の一歩」の不一致を見極めること。 2. 環境への依存を分離する: `window` や `localStorage` を使うなら、必ず `useEffect` か `dynamic(ssr: false)` の壁の内側で実行する。 3. 設計判断: 「動けばいい」という安易な解決ではなく、「ユーザー体験(UX)」と「保守性」のバランスを見ながら技術を選択する。