Next.js Hydration Errorの解決方法【2026年最新版】完全ガイド

スポンサーリンク

Next.js Hydration Errorの解決方法【2026年最新版】完全ガイド

Next.jsで開発していると突然表示される「Hydration failed because the initial UI does not match what was rendered on the server」や「Text content does not match server-rendered HTML」というエラーに悩まされていませんか?この記事では、2026年最新のNext.js 15環境も含め、ハイドレーションエラーの原因と具体的な解決方法を徹底的に解説します。


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

Next.jsのHydration Error(ハイドレーションエラー)は、サーバーサイドレンダリング(SSR)で生成されたHTMLと、クライアント側でReactが最初にレンダリングしようとしたHTMLの間に不一致(ミスマッチ)が生じた場合に発生するエラーです。

ハイドレーションとは何か?

ハイドレーション(Hydration)とは、サーバーで事前に生成された静的なHTMLに対して、ブラウザ上でReactがイベントハンドラやstateを紐付けて「動的な状態」にするプロセスのことです。水分(=インタラクティブ性)を注入するという意味で「ハイドレーション」と呼ばれています。

よく表示されるエラーメッセージ

開発者がコンソールやブラウザで目にする代表的なエラーメッセージは以下の通りです:

  • Hydration failed because the initial UI does not match what was rendered on the server.
  • Text content does not match server-rendered HTML.
  • Error: Hydration failed because the server rendered HTML didn't match the client.
  • Warning: Expected server HTML to contain a matching <div> in <p>.

発生する状況

このエラーは主に以下の場面で発生します:

  • Next.jsアプリの開発中にブラウザのコンソールに赤いエラーが表示される
  • 本番環境でページの一部が正しく表示されない、またはインタラクションが動作しない
  • ビルド後のプレビューで初めて気づくケースも多い
  • Chrome拡張機能がDOMを書き換えることで発生するケースもある

影響範囲として、ハイドレーションエラーが発生すると、Reactはクライアント側でDOMを完全に再構築しようとするため、パフォーマンスの低下を招くことがあります。また、Next.js 15以降(React 19)では、ハイドレーションエラーがより厳密にハンドリングされるようになっており、以前のバージョンでは警告だったものがエラーとして表示される場合もあります。


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

ハイドレーションエラーの原因は多岐にわたりますが、大きく分けて以下の5つのカテゴリに分類できます。

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

サーバー側にはブラウザ環境が存在しないため、windowlocalStoragenavigatordocument などのブラウザ専用オブジェクトをレンダリングロジック内で直接参照すると、サーバーとクライアントで出力が異なります。

// ❌ NG: サーバー側ではwindowが存在しないため不一致が発生
function MyComponent() {
  const width = window.innerWidth; // サーバーではエラーまたはundefined
  return <div>{width > 768 ? 'デスクトップ' : 'モバイル'}</div>;
}

典型的なパターンとして、typeof window !== 'undefined' の条件分岐をレンダリング内で行うと、サーバーとクライアントで異なるJSXを返してしまうためエラーの原因になります。

原因2: 日付・時刻・ランダム値の使用

new Date()Date.now()Math.random()crypto.randomUUID() など、呼び出すたびに異なる値を返す関数をレンダリング中に使用すると、サーバーで生成した値とクライアントで生成した値が一致しません。

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

タイムゾーンの違いも原因になります。サーバーがUTCで動作し、クライアントがJST(UTC+9)の場合、同じ Date オブジェクトでも異なる文字列が生成されます。

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

HTMLの仕様に違反するネスト構造(例:<p> タグの中に <div> タグを配置する)があると、ブラウザがHTMLを自動修正し、サーバーから受け取ったHTMLとの不一致が発生します。

// ❌ NG: <p>の中に<div>は配置できない
function BadNesting() {
  return (
    <p>
      テキスト
      <div>この部分が問題</div>
    </p>
  );
}

ブラウザはHTMLパーサーの規則に従い、不正なネストを検出すると自動的にDOMツリーを再構成します。これにより、サーバーが送信したHTMLとブラウザが解釈したDOMが異なる状態になります。

原因4: Chrome拡張機能によるDOM改変

2026年現在、非常に多くの開発者を悩ませている原因のひとつがChrome拡張機能です。ColorZilla、Grammarly、Google翻訳、パスワードマネージャーなどの拡張機能が、Reactのハイドレーション開始前にDOMに属性や要素を追加することで、サーバーHTMLとの不一致を引き起こします。

特にNext.js 15 + React 19環境では、cz-shortcut-listen="true" のような拡張機能が注入する属性がハイドレーションエラーの直接的な原因として報告されています。

原因5: 外部スクリプトやサードパーティウィジェットの介入

Google Analytics、A/Bテストツール、チャットウィジェット、広告スクリプトなどのサードパーティスクリプトが、Reactのハイドレーション前にDOMを変更することでエラーが発生します。これらのスクリプトは <head><body> に要素を追加・変更するため、サーバーが送信した元のHTMLとの差異が生まれます。


解決方法1: useEffectを使ったクライアント専用レンダリング(推奨)

最も推奨される解決方法は、useEffect フックを使ってクライアント側でのみ値を設定する方法です。useEffect はハイドレーション完了後に実行されるため、サーバーとクライアントのHTMLが一致した状態を保てます。

手順1: stateとuseEffectの組み合わせパターン

ブラウザ専用の値(画面幅、ローカルストレージの値など)を使いたい場合は、初期値をサーバーと同じにし、useEffect 内でクライアント固有の値に更新します。

'use client';
import { useState, useEffect } from 'react';

function ResponsiveComponent() {
  // ステップ1: サーバーと同じ初期値を設定
  const [isMobile, setIsMobile] = useState(false);

  // ステップ2: useEffect内でクライアント固有の値を設定
  useEffect(() => {
    setIsMobile(window.innerWidth < 768);

    const handleResize = () => setIsMobile(window.innerWidth < 768);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <div>
      {isMobile ? <MobileNav /> : <DesktopNav />}
    </div>
  );
}

手順2: マウント状態を追跡するパターン

より汎用的なパターンとして、コンポーネントがクライアント側でマウントされたかどうかを追跡する方法があります。

'use client';
import { useState, useEffect } from 'react';

function ClientOnlyComponent() {
  const [isMounted, setIsMounted] = useState(false);

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

  // マウント前はサーバーと同じ出力(またはスケルトン)
  if (!isMounted) {
    return <div className="skeleton">読み込み中...</div>;
  }

  // マウント後はクライアント固有の内容を表示
  return (
    <div>
      <p>現在時刻: {new Date().toLocaleTimeString('ja-JP')}</p>
      <p>ブラウザ: {navigator.userAgent}</p>
    </div>
  );
}

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

プロジェクト全体で再利用できるように、カスタムフックを作成することをお勧めします。

// hooks/useHydration.ts
'use client';
import { useState, useEffect } from 'react';

export function useHydrated() {
  const [hydrated, setHydrated] = useState(false);

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

  return hydrated;
}

// 使用例
function MyComponent() {
  const hydrated = useHydrated();

  if (!hydrated) return <Skeleton />;

  return <div>{/* クライアント固有のコンテンツ */}</div>;
}

注意点

  • useEffect の初期レンダリングではサーバーと同じ出力を維持することが重要です
  • スケルトンUIやローディング表示を使うことで、ユーザー体験の劣化を最小限に抑えられます
  • この方法はSEOに影響する可能性があるため、検索エンジンに表示させたいコンテンツには慎重に適用してください

解決方法2: dynamic importでSSRを無効化する

useEffect パターンが使いにくい場合や、コンポーネント全体がクライアント専用の場合は、Next.jsの dynamic() を使ってSSRを無効化できます。

基本的な使い方

import dynamic from 'next/dynamic';

// SSRを無効にしてインポート
const ClientOnlyChart = dynamic(
  () => import('../components/Chart'),
  {
    ssr: false,
    loading: () => <p>チャートを読み込み中...</p>
  }
);

export default function Dashboard() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <ClientOnlyChart />
    </div>
  );
}

App Routerでの使用

Next.js 13以降のApp Routerを使用している場合も、dynamic は同様に使用できます。

// app/dashboard/page.tsx
import dynamic from 'next/dynamic';

const MapComponent = dynamic(
  () => import('@/components/Map'),
  { ssr: false }
);

export default function DashboardPage() {
  return (
    <main>
      <h1>地図表示</h1>
      <MapComponent />
    </main>
  );
}

この方法が適しているケース

  • 地図ライブラリ(Leaflet、Google Maps)を使用するコンポーネント
  • グラフ・チャートライブラリ(Chart.js、D3.js)を使用するコンポーネント
  • ブラウザAPIに強く依存するコンポーネント
  • エディタコンポーネント(Monaco Editor、CodeMirrorなど)

SSRを完全に無効化するため、初期HTMLにはコンポーネントの内容が含まれません。SEOが重要なコンテンツにはこの方法は適していません。


解決方法3: HTML構造の修正とブラウザ拡張の対処(上級者向け)

HTMLネスト構造の修正

不正なHTMLネストが原因の場合は、HTML仕様に準拠した構造に修正します。

// ❌ NG: <p>の中に<div>は入れられない
<p>テキスト<div>ブロック要素</div></p>

// ✅ OK: <div>で囲む、または<span>を使用
<div>
  <p>テキスト</p>
  <div>ブロック要素</div>
</div>

// ✅ OK: インライン要素を使用
<p>テキスト<span>インライン要素</span></p>

特に注意が必要なのは、CMSやマークダウンから生成されたHTMLを dangerouslySetInnerHTML で埋め込む場合です。外部ソースのHTMLは不正なネストを含んでいることが多いため、サニタイズ処理を行うことを推奨します。

Chrome拡張機能への対処

開発環境での拡張機能によるハイドレーションエラーには、以下のアプローチが有効です。

方法A: 拡張機能のサイトアクセス権限を制限する

Chromeの拡張機能アイコンを右クリックし、「このサイトでの読み取りと変更の権限」を「クリックした場合のみ」に変更します。これにより、開発中のlocalhostにて拡張機能がDOMを変更するのを防げます。

方法B: 開発専用のブラウザプロファイルを作成する

拡張機能をインストールしていないクリーンなブラウザプロファイルを開発専用に作成することで、拡張機能由来のハイドレーションエラーを完全に回避できます。

方法C: suppressHydrationWarningを限定的に使用する

<body> タグなど、拡張機能が属性を追加しがちな要素に suppressHydrationWarning を設定します。

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

重要: suppressHydrationWarning は1階層分のみ有効で、子要素の不一致は検知されます。また、本番環境で真のバグを隠してしまう可能性があるため、安易な多用は避けてください。

MutationObserverを使った高度な対処

拡張機能が注入する属性をハイドレーション前に除去する方法です。

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function() {
                const observer = new MutationObserver((mutations) => {
                  mutations.forEach((mutation) => {
                    if (mutation.type === 'attributes') {
                      const attr = mutation.attributeName;
                      if (attr && attr.startsWith('cz-')) {
                        mutation.target.removeAttribute(attr);
                      }
                    }
                  });
                });
                observer.observe(document.documentElement, {
                  attributes: true,
                  subtree: true,
                  attributeFilter: ['cz-shortcut-listen']
                });
              })();
            `,
          }}
        />
      </head>
      <body suppressHydrationWarning={true}>
        {children}
      </body>
    </html>
  );
}

エラーを予防するには

ハイドレーションエラーを未然に防ぐためのベストプラクティスを紹介します。

1. レンダリング時の環境依存コードを避ける

コンポーネントのreturn文(レンダリングロジック)内で、環境によって異なる値を生成するコードを使わないようにしましょう。環境依存のロジックはすべて useEffect 内に移動させます。

2. CSS Media Queryを活用する

レスポンシブデザインの切り替えは、JavaScriptの条件分岐ではなく、CSSのMedia Queryで実現するのが最も安全です。

/* JavaScriptの条件分岐よりCSSを優先 */
.mobile-only { display: none; }
.desktop-only { display: block; }

@media (max-width: 768px) {
  .mobile-only { display: block; }
  .desktop-only { display: none; }
}

3. HTMLバリデーションを自動化する

ESLintの jsx-a11y プラグインや、eslint-plugin-react のルールを活用して、不正なHTMLネストをビルド時に検出できるようにしましょう。

4. テスト環境を整備する

開発環境でハイドレーションエラーが出ていないかを確認するために、以下を実施しましょう:

  • next build && next start でプロダクションビルドを定期的にテスト
  • 拡張機能のないクリーンなブラウザプロファイルでテスト
  • Sentryなどのエラー監視ツールでハイドレーションエラーを本番環境で監視

5. サードパーティスクリプトの配置を最適化する

Google Analyticsや広告スクリプトなどは、Next.jsの <Script> コンポーネントの strategy プロパティを使い、適切なタイミングで読み込みましょう。

import Script from 'next/script';

// ハイドレーション後に読み込む
<Script src="https://example.com/analytics.js" strategy="afterInteractive" />

// アイドル時に読み込む
<Script src="https://example.com/widget.js" strategy="lazyOnload" />

まとめ

Next.jsのHydration Errorは、サーバーとクライアント間のレンダリング結果の不一致が原因で発生するエラーです。2026年のNext.js 15 + React 19環境では、以前より厳密にチェックされるため、適切な理解と対処が一層重要になっています。

解決の重要ポイント:

  1. useEffectパターンがほとんどのケースで最も効果的。ブラウザ専用APIや動的な値は必ず useEffect 内で処理する
  2. dynamic import(ssr: false)はコンポーネント全体がクライアント依存の場合に使用する
  3. HTML構造の正しさを常に意識し、<p> の中に <div> を入れるなどの不正ネストを避ける
  4. Chrome拡張機能が原因の場合は、開発用プロファイルの利用や suppressHydrationWarning で対処する
  5. CSS Media Queryを優先し、JavaScriptでの条件分岐によるレイアウト切り替えを避ける

もし上記の方法で解決できない場合は、Next.jsのGitHub Discussionsでエラーメッセージを検索してみてください。同じ問題に直面した開発者の解決策が見つかる可能性があります。また、Sentryなどのエラー監視ツールを導入して、本番環境でのハイドレーションエラーを継続的に監視することも重要です。


参考資料

コメント

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