Next.jsのHydrationエラー(不一致)の原因と直し方|App Router対応(2026)
Next.js App Routerで発生するReact Hydration不一致エラーの原因切り分けと修正手順を解説します。「use client」でもサーバー側で事前レンダーされる仕組み、安全なlocalStorageの扱い方、日付の固定フォーマット、ssr: falseの制約、本番ビルドでの確認手順をまとめました。
目次
- Next.jsのHydration不一致エラーはどう切り分けて直す?
- 先に結論
- 症状
- 原因を切り分ける診断表
- 'use client' を付けるだけでは解決しない理由
- 日付の決定的フォーマット、不正なHTML入れ子、外部要因の切り分け
- シリアライズされたISO文字列から決定的に日付を整形する
- 不正なHTMLの入れ子を直す
- ブラウザ拡張機能とCDNのHTML変換を切り分ける
- ssr: false と suppressHydrationWarning の正しい使いどころ
- ブラウザ専用ウィジェットはClient Component内で ssr: false を使う
- suppressHydrationWarning の制限を理解する
- 修正後の実践的な検証手順
- 参考資料
- 内部リンク
Next.jsのHydration不一致エラーはどう切り分けて直す?
先に結論
Hydration(ハイドレーション)の不一致エラーは、サーバーが生成した初期HTMLと、ブラウザの初回レンダーでReactが構築したツリーが一致しないときに発生します。Reactの hydrateRoot リファレンスおよびNext.jsのHydrationエラー解説ページにあるとおり、Reactはイベントリスナーを接続する際、クライアントの初回レンダー結果がサーバー出力のDOMと同一であることを前提としています。
App Router環境で不一致を解消する基本方針は次の3つです。
- サーバー側の出力とブラウザ初回レンダーの双方で、まったく同じ初期マークアップを返すようにそろえる。
window、localStorage、navigator、閲覧者のタイムゾーンなどブラウザ固有の値は、初期stateを固定したうえでuseEffectの中で読み取る。- 不正なHTMLの入れ子を修正し、ブラウザAPIに強く依存する外部ウィジェットはClient Component内で
next/dynamicのssr: falseを使って分離する。
なお、本番環境でServer Componentが例外を投げた際に表示される error.digest とHydrationエラーを混同しないでください。digest はサーバー側のレンダー自体が失敗したことを示す識別子であり、HydrationエラーはサーバーのHTML出力が成功した後にブラウザ側の初回ツリーと食い違った状態を指します。
症状
開発環境(next dev)では、オーバーレイやコンソールに次のようなメッセージが表示されます。
Hydration failed because the server rendered HTML didn't match the client.
本番ビルドでは圧縮されたReactエラーがコンソールに出力されるほか、読み込み直後にテキストやレイアウトが一瞬切り替わる(フリッカー)、サブツリーが破棄・再生成されて画面がガタつく、初回表示直後にボタンを押しても反応しないといった現象が起きます。
原因を切り分ける診断表
コンポーネント構成を書き換える前に、次の表で原因を特定してください。
| 症状 | 主な原因 | 確認方法 | 直し方 |
| :----------------------------------------------------------- | :---------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------- |
| 読み込み直後に日時・通貨・数値の表示が変わる | レンダー中の Date 生成やタイムゾーン・ロケール未固定のフォーマット | 「ページのソースを表示」の文字列と、ブラウザのローカルタイムゾーン表示を比較する | サーバーからはISO文字列を渡し、明示的な locale と timeZone を指定して整形するか、ローカル変換を useEffect に移す |
| テーマや保存済み設定の読み込み時にちらつく・警告が出る | レンダー本体で typeof window !== 'undefined' 分岐や localStorage 読み取りを行っている | コンポーネントのレンダー関数内に window、document、localStorage、matchMedia がないか検索する | サーバーと一致する固定の初期stateで初回描画し、useEffect 内でストレージの値を反映する |
| <p>、<div>、<table> などの親子関係に関する警告が出る | HTML仕様に反する入れ子をブラウザのHTMLパーサーが自動補正した | DevToolsのElementsパネルで、生のHTMLレスポンスと補正後のDOM構造を見比べる | タグ構造を修正する(<p> の中に <div> や <p> を入れない、<button> の中に別の <button> を入れない) |
| 特定の端末やネットワークでのみ発生し、クリーン環境では消える | ブラウザ拡張機能によるDOM書き換え、またはCDNエッジでのHTML変換 | 拡張機能をすべて無効化したクリーンなプロファイルで再現確認し、オリジン直接取得のHTMLとCDN経由のHTMLを比較する | HTML変換が原因と確認できた場合は、その変換を無効化して再確認する |
'use client' を付けるだけでは解決しない理由
App Routerでよくある誤解が「'use client' を宣言すればブラウザだけで実行される」という思い込みです。Next.jsでは、Client Componentであっても初期HTMLを作るためにサーバー側で一度プリレンダーされ、その後ブラウザでHydrationが行われます(Next.js Hydrationエラーガイド)。
そのため、レンダー関数の中で typeof window !== 'undefined' によって表示内容を分岐すると、サーバー側とブラウザ初回レンダーで別々の枝を通ることになり、表示内容が異なれば不一致が起きます。次のように初期stateを固定し、マウント後の useEffect で localStorage を読み取ってください。また、プライベートブラウジングや厳格なプライバシー設定でストレージアクセスが拒否されても例外で落ちないよう try/catch で囲みます(本記事のコード例は構成を示すための例示です)。
'use client';
import { useEffect, useState } from 'react';
type BannerPrefs = 'expanded' | 'compact';
export function DensityToggle() {
// サーバーのプリレンダーとブラウザ初回レンダーで一致する初期state
const [density, setDensity] = useState<BannerPrefs>('expanded');
useEffect(() => {
try {
const saved = window.localStorage.getItem('ui-density');
if (saved === 'compact' || saved === 'expanded') {
setDensity(saved);
}
} catch {
// ブラウザ設定でlocalStorageがブロックされている場合のSecurityErrorを無視
}
}, []);
return <div data-density={density}>View mode: {density}</div>;
}
日付の決定的フォーマット、不正なHTML入れ子、外部要因の切り分け
シリアライズされたISO文字列から決定的に日付を整形する
レンダー中に new Date().toLocaleDateString() を呼ぶと、サーバーの時計・ロケールと閲覧者のブラウザの時計・ロケールが食い違います。データ層からはシリアライズ済みのISO 8601文字列を渡し、初回レンダーで表示する場合はロケールと timeZone の両方を明示的に固定します。
const utcFormatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeZone: 'UTC',
});
export function ReleaseDate({ publishedAtIso }: { publishedAtIso: string }) {
return <time dateTime={publishedAtIso}>{utcFormatter.format(new Date(publishedAtIso))}</time>;
}
不正なHTMLの入れ子を直す
ブラウザはReactがHydrationを始める前に、仕様外のHTMLを自動的に組み替えます。たとえば <p> の中に <div> や <section> を置いたり、<table> 直下に <tbody> を省略して <tr> を並べたりすると、ブラウザが要素を分割・補完するため、Reactの仮想DOMと構造がずれてエラーになります。警告を抑え込むのではなく、HTMLタグの階層自体を修正してください。
ブラウザ拡張機能とCDNのHTML変換を切り分ける
パスワードマネージャー、翻訳ツール、ダークモード拡張機能などは、Hydration完了前に属性やDOMノードを注入することがあります。また、CDNのエッジ機能(メールアドレス難読化、HTMLの自動改変・スクリプト挿入など)がテキストノードを書き換えるケースもあります。調査時は必ず拡張機能をすべて無効化したクリーンなブラウザプロファイルで確認し、curl 等でオリジンサーバーの出力とCDN経由の出力を比較して切り分けてください。
ssr: false と suppressHydrationWarning の正しい使いどころ
ブラウザ専用ウィジェットはClient Component内で ssr: false を使う
キャンバス描画やDOMサイズ計測など、import時点や初回描画でブラウザAPIが必須となる小さな外部ウィジェットは、next/dynamic の ssr: false で遅延読み込みします。Next.jsのLazy Loadingガイドに記載されているとおり、ssr: false は Client Component('use client')の内部でのみ使用可能 であり、Server Component内で直接指定するとビルドエラーになります。
'use client';
import dynamic from 'next/dynamic';
const ViewportChart = dynamic(() => import('./ViewportChart'), {
ssr: false,
loading: () => <div className="h-48 rounded border" aria-busy="true" />,
});
export function MetricsPanel() {
return <ViewportChart />;
}
SSRを無効化したウィジェット自体はJavaScriptが動くまで内容を表示できません。レンダー中の Math.random() や crypto.randomUUID() も避け、安定した値を渡します。ページ全体をクライアントレンダーに落とさないよう、ssr: false は対象の末端ウィジェットだけに限定し、レイアウトシフトを防ぐプレースホルダー(loading)を添えます。
suppressHydrationWarning の制限を理解する
ローカル時刻の相対表示など、単一要素のテキストや属性がサーバーとクライアントでどうしても異なる場合に限り、suppressHydrationWarning を指定できます。ただし、Reactの hydrateRoot ドキュメントが警告しているとおり、この属性が効くのは直下の1階層のみであり、さらにReactは不一致となったテキスト内容をHydration時に自動で書き換え(パッチ)しません。ロジックの不備を隠すために使うと、次のstate更新が起きるまでサーバー側の古い文字列が画面に残り続けるため、安易な多用は避けてください。
修正後の実践的な検証手順
クライアント側のソフトナビゲーション(<Link> による遷移)では初期HTMLのHydrationが走らないため、next dev での画面遷移だけで修正完了と判断してはいけません。
- 本番ビルドを作成して起動する(
npm run build && npm run start)。 - 拡張機能をすべて無効化したクリーンなブラウザプロファイルで対象URLを開く。
- ハードリロード(
Ctrl+Shift+R/Cmd+Shift+R)を実行し、サーバーから新しいHTMLを取得させて最初からhydrateRootを走らせる。 - 次の2点を両方確認する。
- コンソールにHydrationの警告・エラーが1件も出ないこと。
- 表示内容と操作が正しいこと。マウント後に保存済み設定を反映する意図的な更新と、意図しないちらつき・古い文字列の残存を区別すること。
参考資料
- React Hydration Error (
react-hydration-error) — Next.js Docs hydrateRootリファレンス — React Docs- Lazy Loading (
next/dynamicとssr: false) — Next.js App Router Guide - リポジトリ参照バージョン:Next.js 15.1.9、React 19.0.0(App Router)。これらは本リポジトリの固定依存バージョンを示すものであり、最新の推奨サポートバージョンを主張するものではありません。また、記事内のコードは説明用の例示です。