Hydration Error が出たらまずタイムゾーンを疑え
Note: この記事は実際の調査事項をもとに、AI とともに執筆しました。
TL;DR
- Next.js SSG + React 19 で本番ビルドのみ発生する謎の hydration error
- 原因は日付フォーマットがローカルタイムゾーンに依存していたのが原因
- ビルド時(GitHub Actions = UTC)とクライアント(ユーザーのブラウザ = JST)でタイムゾーンが異なり、日付境界付近の日時で表示が変わる
-
suppressHydrationWarningで解決できる
Note: この問題は
date-fns特有ではなく、ローカルタイムゾーンで日付をフォーマットする処理全般(toLocaleDateString()、Intl.DateTimeFormat()、dayjs、momentなど)で発生する可能性があります。
Hydration(ハイドレーション)とは
SSR(Server-Side Rendering)や SSG(Static Site Generation)では、事前に HTML を生成します。
- SSR: リクエストごとにサーバー側で HTML を生成
- SSG: ビルド時に HTML を事前生成(この記事のケース)
SSG の場合、ビルド時に生成された HTML は静的ファイルとして保存され、ブラウザに配信されてすぐに表示されます。
しかし、この段階では HTML は単なる静的なマークアップです。見た目は表示されますが、React のイベントハンドラはまだ登録されていないため、ボタンをクリックしても何も起きません。
Hydration は、この静的 HTML に React を「接続」して、インタラクティブにするプロセスです。
SSG の場合:
ビルド時: HTML を事前生成 → 静的ファイルとして保存
↓
ブラウザ: HTML を表示(まだ静的)
↓
React: Hydration 実行 → イベントリスナー登録、状態管理開始
↓
ユーザー: インタラクティブなページを操作できる
なぜ Hydration が必要なのか
SSR/SSG のメリット:
- 初期表示が速い(HTML がすぐ表示される)
- SEO に有利(クローラーが HTML を読める)
React SPA のメリット:
- インタラクティブ(状態管理、イベント処理)
Hydration は両方のメリットを得るための仕組みです。最初は静的 HTML で高速表示し、その後 React が引き継いでインタラクティブにします。
Hydration Error とは
React が Hydration を実行する際、ビルド時(SSG の場合)またはサーバーで生成した HTML とクライアントで生成しようとする HTML を比較します。
もし内容が異なる場合、以下のようになります:
ビルド時の HTML(SSG の場合): <time>2025-12-28</time>
クライアント: <time>2025-12-29</time> ← 違う!
Hydration Error #418: "Text content does not match server-rendered HTML"
React は「ビルド時(SSG の場合)またはサーバーとクライアントで同じ HTML が生成されるはず」と期待しているため、不一致があるとエラーになります。
よくある Hydration Error の原因
| 原因 | 例 |
|---|---|
| タイムゾーンの違い |
format(date) がビルド時と実行時で異なる |
| 現在時刻の使用 |
new Date() がビルド時と実行時で異なる |
| ランダム値 |
Math.random() や uuid()
|
| ブラウザ固有の API |
window.innerWidth、localStorage
|
| 拡張機能による DOM 改変 | ブラウザ拡張が HTML を変更 |
発生した問題
Next.js 16 + React 19 の SSG プロジェクトで、本番ビルド時のみトップページで以下のエラーが発生しました:
Uncaught Error: Minified React error #418;
visit https://react.dev/errors/418?args[]=text&args[]=
特徴:
- 開発サーバー (
npm run dev) では発生しない - 本番ビルド (
npm run build && serve out) でのみ発生 - トップページでのみ発生
調査過程
React Error #418 とは
React のエラーページ によると:
Text content does not match server-rendered HTML.
ビルド時(SSG の場合)またはサーバーサイドでレンダリングされた HTML と、クライアントサイドで hydrate しようとした内容が一致しないエラーです。
エラーメッセージに visit https://react.dev/errors/418 と表示されるのは、このエラーコードに対応する詳細な説明ページへのリンクです。
原因:Time コンポーネント
使用している Time コンポーネントを調査しました:
// Time.tsx
import { format } from "date-fns";
export default function Time({ dateTime }: { dateTime: string }) {
return (
<time dateTime={dateTime}>
{format(new Date(dateTime), "yyyy-MM-dd")}
</time>
);
}
format() はローカルタイムゾーンで日付をフォーマットします!
これは date-fns に限らず、多くの日付フォーマット関数の標準的な動作です。
なぜタイムゾーンが問題になるのか
ビルド環境とクライアント環境の違い
この記事のケースでは、SSG のビルドプロセスを GitHub Actions で実行していました。
| 環境 | タイムゾーン |
|---|---|
| GitHub Actions(ビルド時) | UTC |
| ユーザーのブラウザ(実行時) | JST (UTC+9) |
日付境界付近での不一致
例えば、2025-12-28T15:00:00Z という日時を考えます:
ビルド時(UTC):
2025-12-28 15:00:00 UTC
↓ format("yyyy-MM-dd")
"2025-12-28"
クライアント(JST):
2025-12-28 15:00:00 UTC = 2025-12-29 00:00:00 JST
↓ format("yyyy-MM-dd")
"2025-12-29"
日付が1日ズレます!
これが hydration mismatch の原因でした。
解決策
方法1: suppressHydrationWarning(採用)
最もシンプルな解決策。React に「この要素の hydration mismatch は無視して」と伝えることです:
// Time.tsx
export default function Time({ dateTime }: { dateTime: string }) {
return (
<time
dateTime={dateTime}
suppressHydrationWarning
>
{format(new Date(dateTime), "yyyy-MM-dd")}
</time>
);
}
メリット:
- 最小限の変更
- 表示はクライアントのタイムゾーンに合わせられる(ユーザーにとって自然)
デメリット:
- 本質的な解決ではない
- ビルド時の HTML とクライアントの表示が一瞬異なる可能性
方法2: 特定のタイムゾーンで統一フォーマット
import { formatInTimeZone } from "date-fns-tz";
export default function Time({ dateTime }: { dateTime: string }) {
return (
<time dateTime={dateTime}>
{formatInTimeZone(new Date(dateTime), "Asia/Tokyo", "yyyy-MM-dd")}
</time>
);
}
メリット:
- 指定したタイムゾーン(この例では JST)で統一されるため、ビルド時とクライアントで一致します
- 日本のユーザーにとって自然な日付表示
デメリット:
- タイムゾーンを固定するため、海外ユーザーには不自然な日付になる可能性があります
教訓
1. 開発環境と本番環境の違いに注意
開発サーバーでは問題なくても、本番ビルドで問題が発生することがあります。特に SSG では、ビルド時とランタイムの環境差異に注意が必要です。
2. 日付処理はタイムゾーンを意識する
多くの日付フォーマット関数はローカルタイムゾーンを使用します。これは意図した動作ですが、SSR/SSG では問題になる可能性があります。
3. suppressHydrationWarning は対症療法
根本解決ではありませんが、ユーザー体験を損なわない場合は有効な選択肢です。
4. React 19 は hydration チェックが厳格
React 18 以前では警告で済んでいた mismatch が、React 19 ではエラーになることがあります。
環境
- Next.js 16.1.1
- React 19.2.3
- date-fns 4.1.0
- TypeScript 5.9.3
Discussion