Next.js「Hydration failed because the initial UI does not match」エラーの解決方法【2026年最新版】

スポンサーリンク

Next.js「Hydration failed because the initial UI does not match」エラーの解決方法【2026年最新版】

Next.jsで開発中に突然表示される「Hydration failed because the initial UI does not match what was rendered on the server」というエラー。React 19とNext.js 15のApp Router環境で特に頻発するこのエラーに悩まされている開発者は非常に多く、GitHub Discussionsでは数千件のコメントが寄せられ、Stack Overflowでも常にトレンド入りしています。本記事では、2026年最新の環境に対応した原因の特定方法と具体的な解決策を、実際のコード例とともに徹底的に解説します。

このエラーとは?発生する症状

Next.jsにおけるHydration Error(ハイドレーションエラー)は、サーバーサイドでレンダリングされたHTMLと、クライアントサイドでReactが最初にレンダリングしようとしたHTMLの内容が一致しない場合に発生するエラーです。

具体的には、ブラウザの開発者コンソールに以下のようなエラーメッセージが表示されます。

Unhandled Runtime Error
Error: Hydration failed because the initial UI does not match what was rendered on the server.

または、以下のようなバリエーションで表示されることもあります。

Error: Text content does not match server-rendered HTML.
Error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.

このエラーが発生すると、Next.jsはサーバーサイドレンダリング(SSR)の結果を破棄し、ページ全体をクライアントサイドで再レンダリングします。これにより、以下のような深刻な影響が生じます。

  • パフォーマンスの大幅な低下: SSRの利点が完全に失われ、初期表示が遅くなる
  • SEOへの悪影響: 検索エンジンのクローラーが正しいHTMLを取得できない可能性がある
  • ユーザー体験の劣化: 画面がちらつく(フラッシュ)現象が発生する
  • 開発者体験の悪化: コンソールに大量のエラーが表示され、他のエラーを見逃しやすくなる

特にNext.js 15とReact 19の組み合わせでは、ハイドレーションの整合性チェックがより厳格になっており、以前のバージョンでは警告で済んでいたものがエラーとして扱われるケースが増えています。

このエラーが発生する原因

原因1: ブラウザ専用APIの使用(window / localStorage / document)

最も頻繁に見られる原因は、サーバーサイドには存在しないwindowlocalStoragedocumentなどのブラウザ専用APIをレンダリング中に直接参照しているケースです。

サーバーサイドでは、これらのオブジェクトはundefinedです。そのため、typeof window !== 'undefined'のような条件分岐をレンダリングロジック内で使うと、サーバーとクライアントで出力が異なり、ハイドレーションエラーが発生します。

// NG: ハイドレーションエラーが発生する
function MyComponent() {
  const isClient = typeof window !== 'undefined';
  return <div>{isClient ? 'クライアント' : 'サーバー'}</div>;
}

原因2: 時刻やランダム値など非決定的な値の使用

new Date()Math.random()のように、実行するたびに異なる値を返す処理をレンダリング内で使用すると、サーバーとクライアントで結果が一致しません。

// NG: サーバーとクライアントでタイムスタンプが異なる
function Clock() {
  return <p>現在時刻: {new Date().toLocaleTimeString()}</p>;
}

サーバーでのレンダリング時刻とクライアントでのハイドレーション時刻にはタイムラグがあるため、当然ながら表示される時刻が異なり、テキスト内容の不一致としてエラーになります。

原因3: 不正なHTMLネスト構造

HTMLの仕様に反するネスト構造(例:<p>タグの中に<div>を配置する)があると、ブラウザのHTMLパーサーがDOMを自動修正し、サーバーでレンダリングしたHTML構造と異なるDOMが生成されます。

// NG: <p>の中に<div>は配置できない
function BadNesting() {
  return (
    <p>
      テキスト
      <div>これは不正なネスト</div>
    </p>
  );
}

ブラウザはこの不正なHTMLを自動修正するため、サーバーが送信したHTMLとブラウザのDOMが一致しなくなります。

原因4: ブラウザ拡張機能によるDOM操作

ColorzillaやGrammarlyなどのブラウザ拡張機能が、ページのDOMに属性やHTML要素を注入するケースがあります。例えば、Colorzillaは<body>タグにcz-shortcut-listen="true"という属性を追加します。これはNext.js 15 + React 19の環境で特に問題になることが報告されています。

原因5: サードパーティスクリプトやCMSコンテンツの不整合

外部のスクリプト(広告タグ、アナリティクスなど)がDOMを変更したり、CMSから取得したHTMLコンテンツに不正なネスト構造が含まれていたりすると、ハイドレーションエラーを引き起こす可能性があります。

解決方法1: useEffectフックでクライアント専用処理を分離する(推奨)

最も効果的で推奨される解決方法は、ブラウザ依存の処理をuseEffectフック内に移動し、初回レンダリングでは確定的(決定論的)な値のみを出力することです。

手順1: クライアント状態の管理パターンを実装する

まず、「サーバーとクライアントで同一の初期値」を持つStateを定義し、useEffectでクライアント側の値に更新します。

'use client';

import { useState, useEffect } from 'react';

function ThemeProvider({ children }) {
  // 初期値はサーバーでも安全な値にする
  const [theme, setTheme] = useState('light');
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    // クライアントでのみlocalStorageを参照
    const savedTheme = localStorage.getItem('theme') || 'light';
    setTheme(savedTheme);
    setMounted(true);
  }, []);

  // マウント前はサーバーと同じ出力を返す
  if (!mounted) {
    return <div data-theme="light">{children}</div>;
  }

  return <div data-theme={theme}>{children}</div>;
}

手順2: 時刻やランダム値はuseEffect内で設定する

'use client';

import { useState, useEffect } from 'react';

function Clock() {
  const [currentTime, setCurrentTime] = useState('');

  useEffect(() => {
    const updateTime = () => {
      setCurrentTime(new Date().toLocaleTimeString());
    };
    updateTime();
    const timer = setInterval(updateTime, 1000);
    return () => clearInterval(timer);
  }, []);

  // 初回レンダリングでは空文字を表示(サーバーと一致)
  return <p>現在時刻: {currentTime || '読み込み中...'}</p>;
}

手順3: カスタムフックで再利用可能にする

このパターンを複数のコンポーネントで使う場合は、カスタムフックに抽出しましょう。

'use client';

import { useState, useEffect } from 'react';

// マウント状態を管理するカスタムフック
function useHasMounted() {
  const [hasMounted, setHasMounted] = useState(false);
  useEffect(() => {
    setHasMounted(true);
  }, []);
  return hasMounted;
}

// 使用例
function ClientOnlyComponent() {
  const hasMounted = useHasMounted();

  if (!hasMounted) {
    return <div>読み込み中...</div>;
  }

  return <div>ウィンドウ幅: {window.innerWidth}px</div>;
}

注意点

  • useEffectはサーバーサイドでは実行されないため、その中でブラウザAPIを安全に使用できます
  • 初回レンダリング時にはプレースホルダーやフォールバックUIを表示することで、ユーザー体験を維持しましょう
  • このパターンでは一瞬プレースホルダーが見えることがありますが、ハイドレーションエラーによるページ全体の再レンダリングよりもはるかに軽量です

解決方法2: next/dynamicによるSSR無効化

useEffectパターンでは対応しにくいコンポーネント(例:ブラウザAPIに強く依存するサードパーティライブラリ)の場合、next/dynamicを使ってSSR自体を無効化する方法が有効です。

import dynamic from 'next/dynamic';

// SSRを無効化してインポート
const MapComponent = dynamic(
  () => import('../components/Map'),
  {
    ssr: false,
    loading: () => <div className="map-placeholder">地図を読み込み中...</div>
  }
);

// 使用例
function LocationPage() {
  return (
    <div>
      <h1>店舗所在地</h1>
      <MapComponent />
    </div>
  );
}

この方法は特に以下のケースで有効です。

  • 地図ライブラリ(Leaflet、Google Maps等):windowオブジェクトに依存するため
  • リッチテキストエディタ(Quill、TipTap等):documentオブジェクトを直接操作するため
  • チャートライブラリ(Chart.js、D3.js等):SVGやCanvasのDOM操作を行うため

ただし、SSRを無効化したコンポーネントは初期HTMLに含まれないため、SEOが重要なコンテンツには使用を避けてください。メインコンテンツ以外のインタラクティブな要素(地図、チャート、エディタ等)に限定して使うのがベストプラクティスです。

解決方法3: HTMLネスト構造の修正とsuppressHydrationWarningの適切な使用

上級者向けの解決策として、HTMLのネスト構造を正しく修正し、やむを得ないケースではsuppressHydrationWarningを使用する方法があります。

不正なHTMLネストの修正

まず、不正なHTMLネストがないかチェックします。以下は代表的な不正パターンとその修正例です。

// NG: <p>内に<div>
<p>テキスト<div>ブロック要素</div></p>

// OK: <div>に変更
<div>テキスト<div>ブロック要素</div></div>

// NG: <a>内に<a>
<a href="/parent">
  親リンク
  <a href="/child">子リンク</a>
</a>

// OK: ネストを解消
<div>
  <a href="/parent">親リンク</a>
  <a href="/child">子リンク</a>
</div>

suppressHydrationWarningの限定的な使用

どうしてもサーバーとクライアントで値が異なる必要がある小さな要素(タイムスタンプ表示など)には、suppressHydrationWarningを使用できます。

// タイムスタンプのように、差異が許容される場合のみ使用
<time suppressHydrationWarning>
  {new Date().toLocaleDateString()}
</time>

重要な注意: suppressHydrationWarningはエラーを非表示にするだけで、根本的な問題を解決しません。テキストノード1階層分のみに適用され、子要素の不一致は検知されたままです。乱用せず、本当にやむを得ない場合にのみ使用してください。

ブラウザ拡張機能への対処

ブラウザ拡張機能が原因の場合は、<body>タグにsuppressHydrationWarningを追加する方法があります。

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body suppressHydrationWarning>{children}</body>
    </html>
  );
}

これにより、拡張機能がbodyに追加する属性による警告を抑制できます。ただし、body直下の子要素の不一致は引き続き検知されます。

エラーを予防するには

ハイドレーションエラーを未然に防ぐために、以下の予防策を日常的な開発に取り入れましょう。

1. 初回レンダリングを「決定的」に保つ

最も重要な原則は、サーバーとクライアントの初回レンダリングで同一の出力を保証することです。環境に依存する値(ブラウザAPI、時刻、乱数等)は、必ずuseEffect内に配置してください。

2. Server ComponentsとClient Componentsを明確に分離する

Next.js 15のApp Routerでは、コンポーネントはデフォルトでServer Componentです。ブラウザAPIやReactフック(useStateuseEffect等)を使う場合は、ファイルの先頭に'use client'ディレクティブを忘れずに追加してください。

3. ESLintルールの活用

eslint-plugin-reactには、不正なHTMLネスト(jsx-no-invalid-html-nestingなど)を検出するルールがあります。CIパイプラインに組み込んで自動的にチェックしましょう。

4. 定期的なテスト

シークレットモード(プライベートブラウジング)やブラウザ拡張機能を無効にした環境で定期的にテストすることで、拡張機能由来のエラーを切り分けられます。

5. CSS駆動のレスポンシブデザイン

ビューポートサイズに応じてマークアップを分岐するのではなく、CSSメディアクエリやdisplay: noneを使って表示を切り替えることで、サーバーとクライアントのHTML構造を一致させられます。

// NG: JSでビューポート分岐
{isMobile ? <MobileNav /> : <DesktopNav />}

// OK: CSSで表示切替
<MobileNav className="block md:hidden" />
<DesktopNav className="hidden md:block" />

まとめ

Next.jsのHydration Error「Hydration failed because the initial UI does not match what was rendered on the server」は、SSR/SSGとクライアントサイドレンダリングの不一致から発生する、Next.js開発で最もよく遭遇するエラーの一つです。

重要ポイントの振り返り:

  • エラーの本質は「サーバーとクライアントの初回レンダリング結果が異なること」
  • 最も多い原因は、ブラウザ専用APIの直接使用、非決定的な値のレンダリング、不正なHTMLネスト
  • 推奨解決策はuseEffectによる段階的レンダリング、必要に応じてnext/dynamicでSSR無効化
  • suppressHydrationWarningは最終手段として限定的に使用
  • 予防策として、初回レンダリングの決定性を保ち、Server/Client Componentを明確に分離する

もしこれらの解決方法を試しても問題が解決しない場合は、以下を確認してください。

  1. Next.jsとReactを最新バージョンにアップデートする
  2. node_modules.nextフォルダを削除して再ビルドする
  3. Next.jsのGitHub Discussions(https://github.com/vercel/next.js/discussions)で同様の事例を検索する
  4. 最小限の再現コードを作成し、Issueとして報告する

参考資料

コメント

タイトルとURLをコピーしました