Next.js Hydration mismatchエラーの原因と解決方法【完全ガイド】
はじめに
Next.jsを使用していて「Hydration mismatch」というエラーが突然発生したことはありませんか?このエラーは、多くのNext.js開発者が経験する一般的な問題です。しかし、原因と解決方法を理解すれば、簡単に対処できます。
本記事では、Hydration mismatchエラーの仕組みから実践的な解決方法まで、初心者向けにわかりやすく解説します。
Hydration mismatchとは?
Hydration mismatchエラーを理解するには、まずNext.jsの動作原理を知る必要があります。
Next.jsのレンダリング過程
Next.jsはサーバーサイドレンダリング(SSR)またはスタティックジェネレーション(SSG)を行います。
- サーバー側:HTMLを生成
- ブラウザに送信:完成したHTMLをクライアントに送る
- ハイドレーション: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)の使用
サーバー環境にはwindowやdocumentオブジェクトが存在しません。これらの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の仕組みを理解すれば簡単に解決できる問題です。重要なポイントをまとめます。
- 原因の理解:サーバーとクライアントのレンダリング結果が異なることが原因
- useEffectの活用:ブラウザ限定の処理はuseEffectで実行
- 初期値の設定:useStateの初期値はサーバー側でも生成できる値を使用
- 動的インポート:クライアント限定のコンポーネントは
dynamicで管理 - 根本的な解決:suppressHydrationWarningは一時的な対策に過ぎない
これらの方法を活用することで、Hydration mismatchエラーは確実に解決できます。最初は手間に感じるかもしれませんが、パターンを理解すれば、次からはすぐに対処できるようになります。
Next.jsの開発をスムーズに進めるために、本記事で紹介した解決方法をぜひ実践してみてください。

