🚰

Hydration Error が出たらまずタイムゾーンを疑え

に公開

Note: この記事は実際の調査事項をもとに、AI とともに執筆しました。

TL;DR

  • Next.js SSG + React 19 で本番ビルドのみ発生する謎の hydration error
  • 原因は日付フォーマットがローカルタイムゾーンに依存していたのが原因
  • ビルド時(GitHub Actions = UTC)とクライアント(ユーザーのブラウザ = JST)でタイムゾーンが異なり、日付境界付近の日時で表示が変わる
  • suppressHydrationWarning で解決できる

Note: この問題は date-fns 特有ではなく、ローカルタイムゾーンで日付をフォーマットする処理全般(toLocaleDateString()Intl.DateTimeFormat()dayjsmoment など)で発生する可能性があります。

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.innerWidthlocalStorage
拡張機能による 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