Next.js Hydration mismatchエラーの原因と解決方法【完全ガイド】

React / Next.js

Next.js Hydration mismatchエラーの原因と解決方法【完全ガイド】

はじめに

Next.jsを使用していて「Hydration mismatch」というエラーが突然発生したことはありませんか?このエラーは、多くのNext.js開発者が経験する一般的な問題です。しかし、原因と解決方法を理解すれば、簡単に対処できます。

本記事では、Hydration mismatchエラーの仕組みから実践的な解決方法まで、初心者向けにわかりやすく解説します。

Hydration mismatchとは?

Hydration mismatchエラーを理解するには、まずNext.jsの動作原理を知る必要があります。

Next.jsのレンダリング過程

Next.jsはサーバーサイドレンダリング(SSR)またはスタティックジェネレーション(SSG)を行います。

  1. サーバー側:HTMLを生成
  2. ブラウザに送信:完成したHTMLをクライアントに送る
  3. ハイドレーション:Reactがブラウザ側でコンポーネントを「活性化」し、イベントリスナーを追加

Hydration mismatchは、この過程でサーバーが生成したHTMLとブラウザがレンダリングするHTMLが異なる場合に発生します。

Hydration mismatchの主な原因

1. 日付・時刻の使用

最も一般的な原因です。サーバー側とクライアント側で異なる時刻を使用すると、レンダリング結果が異なります。

// ❌ エラーの例
export default function DateComponent() {
  const currentDate = new Date().toLocaleDateString();
  return <p>{currentDate}</p>;
}

上記のコードは、サーバーサイドとクライアントサイドで異なる日付を表示するため、Hydration mismatchが発生します。

2. ブラウザ限定API(window、document)の使用

サーバー環境にはwindowdocumentオブジェクトが存在しません。これらのAPI直接使用するとエラーが発生します。

// ❌ エラーの例
export default function BrowserAPI() {
  const width = window.innerWidth; // サーバー側では存在しない
  return <p>Window width: {width}</p>;
}

3. 乱数生成

サーバーとクライアントで異なる乱数が生成されます。

// ❌ エラーの例
export default function RandomComponent() {
  const randomId = Math.random();
  return <div id={randomId}</div>;
}

4. 条件付きレンダリング

クライアント側でのみ表示される条件がある場合、HTMLが異なります。

5. useStateの初期値が依存する外部情報

useStateの初期値がプロップスやグローバル状態に依存する場合、同期が取れないことがあります。

解決方法【ステップバイステップ】

方法1:useEffectを使用する(最も推奨)

ブラウザ限定の操作はuseEffectで実行し、ハイドレーション後に実行させます。

// ✅ 正しい例
import { useState, useEffect } from 'react';

export default function DateComponent() {
  const [date, setDate] = useState('');

  useEffect(() => {
    // ハイドレーション後にクライアント側でのみ実行
    setDate(new Date().toLocaleDateString());
  }, []);

  return <p>{date || 'Loading...'}</p>;
}

ポイント:

  • useEffectはハイドレーション後にのみ実行される
  • 初期値を空文字列にし、ハイドレーション時には表示しない
  • 読み込み中の状態を管理する

方法2:suppressHydrationWarningを使用

警告を無視したい場合、該当の要素にsuppressHydrationWarning属性を追加します。ただし、根本的な解決ではないため注意が必要です。

// ⚠️ 一時的な対策
export default function DateComponent() {
  const currentDate = new Date().toLocaleDateString();
  return <p suppressHydrationWarning>{currentDate}</p>;
}

方法3:動的インポートで回避

特定のコンポーネントをクライアント側でのみレンダリングします。

// pages/index.js
import dynamic from 'next/dynamic';

const ClientOnlyComponent = dynamic(
  () => import('../components/DateComponent'),
  { ssr: false }
);

export default function Home() {
  return (
    <>
      <h1>Welcome</h1>
      <ClientOnlyComponent />
    </>
  );
}

注意:ssr: falseを設定するとサーバーサイドレンダリングのメリットが失われるため、慎重に使用してください。

方法4:useLayoutEffectで同期する

ペイント前に状態を同期させたい場合はuseLayoutEffectを使用します。

import { useState, useLayoutEffect } from 'react';

export default function WindowSize() {
  const [width, setWidth] = useState(0);

  useLayoutEffect(() => {
    setWidth(window.innerWidth);
  }, []);

  return <p>Window width: {width}</p>;
}

実践的なコード例

例1:タイムスタンプの表示

// components/Timestamp.js
import { useState, useEffect } from 'react';

export default function Timestamp() {
  const [timestamp, setTimestamp] = useState(null);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
    setTimestamp(new Date().toISOString());
  }, []);

  if (!isClient) return null;

  return (
    <div>
      <p>Current timestamp: {timestamp}</p>
    </div>
  );
}

例2:ウィンドウサイズの取得

// components/ResponsiveComponent.js
import { useState, useEffect } from 'react';

export default function ResponsiveComponent() {
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    // 初期値を設定
    setDimensions({
      width: window.innerWidth,
      height: window.innerHeight
    });

    // リサイズイベントにリスナーを追加
    const handleResize = () => {
      setDimensions({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <div>
      <p>Width: {dimensions.width}px</p>
      <p>Height: {dimensions.height}px</p>
    </div>
  );
}

例3:localStorageの使用

// components/ThemeSwitcher.js
import { useState, useEffect } from 'react';

export default function ThemeSwitcher() {
  const [theme, setTheme] = useState('light');
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    // ハイドレーション完了後に実行
    setIsMounted(true);
    const savedTheme = localStorage.getItem('theme') || 'light';
    setTheme(savedTheme);
  }, []);

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);
  };

  if (!isMounted) return null;

  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

よくある間違いと対策

間違い1:useStateの初期値にブラウザAPIを使用

// ❌ 間違い
const [width, setWidth] = useState(window.innerWidth);

// ✅ 正しい
const [width, setWidth] = useState(0);
useEffect(() => {
  setWidth(window.innerWidth);
}, []);

間違い2:条件付きレンダリングで異なる要素構造を返す

// ❌ 間違い
export default function Component() {
  if (typeof window === 'undefined') {
    return <div>Server</div>;
  }
  return <div>Client</div>;
}

// ✅ 正しい
export default function Component() {
  const [isClient, setIsClient] = useState(false);
  
  useEffect(() => {
    setIsClient(true);
  }, []);
  
  return <div>{isClient ? 'Client' : 'Server'}</div>;
}

間違い3:無限ループの作成

// ❌ 間違い - 無限ループ
useEffect(() => {
  setState(new Date());
  // 依存配列がないため、毎回実行される
});

// ✅ 正しい
useEffect(() => {
  setState(new Date());
}, []); // 依存配列を指定

間違い4:suppressHydrationWarningの過度な使用

根本的な原因を解決せずにsuppressHydrationWarningで警告を無視することは避けましょう。これはバグを隠すだけで、実際の問題は解決しません。

デバッグのコツ

1. ブラウザのコンソール確認

Hydration mismatchエラーが発生すると、ブラウザのコンソールに詳細なメッセージが表示されます。どの要素で問題が発生しているか確認しましょう。

2. SSRとCSRの動作の違いを理解

Next.jsの開発サーバーを再起動して、SSR時の動作を確認することが重要です。

3. next/dynamicの活用

クライアント限定のコンポーネントは、dynamicで明示的にマークすることでバグを防げます。

パフォーマンス最適化のポイント

Hydration mismatchを解決する際は、パフォーマンスにも注意が必要です。

  • useEffectの過度な使用:複数の状態更新は、1つのuseEffectで管理する
  • 不要な再レンダリング:useCallbackやuseMemoで最適化する
  • ローディング状態の表示:UXの観点から、読み込み中の状態を適切に表示する

Next.js 13以降での変更

Next.js 13以降では、App Routerが導入され、Hydration mismatchの扱いが変わりました。

// app/components/DateComponent.js
'use client'; // クライアントコンポーネントとして明示

import { useState, useEffect } from 'react';

export default function DateComponent() {
  const [date, setDate] = useState('');

  useEffect(() => {
    setDate(new Date().toLocaleDateString());
  }, []);

  return <p>{date || 'Loading...'}</p>;
}

‘use client’ディレクティブを使用することで、明示的にクライアント側でレンダリングするコンポーネントを指定できます。

まとめ

Hydration mismatchエラーは、Next.jsの仕組みを理解すれば簡単に解決できる問題です。重要なポイントをまとめます。

  1. 原因の理解:サーバーとクライアントのレンダリング結果が異なることが原因
  2. useEffectの活用:ブラウザ限定の処理はuseEffectで実行
  3. 初期値の設定:useStateの初期値はサーバー側でも生成できる値を使用
  4. 動的インポート:クライアント限定のコンポーネントはdynamicで管理
  5. 根本的な解決:suppressHydrationWarningは一時的な対策に過ぎない

これらの方法を活用することで、Hydration mismatchエラーは確実に解決できます。最初は手間に感じるかもしれませんが、パターンを理解すれば、次からはすぐに対処できるようになります。

Next.jsの開発をスムーズに進めるために、本記事で紹介した解決方法をぜひ実践してみてください。

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