Loading

ROUTE

ルートゼロの
アクティビティ

Next.jsのHydration Mismatch|Date・Math.randomの直し方

2

Next.jsのHydration Mismatchは、サーバーで生成したHTMLと、ブラウザ側の最初のReact renderが一致しないときに起きます。`Date.now()`や`new Date()`、`Math.random()`のようにrenderのたびに結果が変わる値をJSX内で直接計算すると、同じComponentでもサーバーとクライアントで別の文字列・IDになりやすいため要注意です。まず「初回renderが両側で同じか」を確認し、必要なら値をサーバーから渡す、hydration後に`useEffect`で更新する、ID用途なら`useId`へ置き換える、という順で直します。

まず結論:Hydrationでは最初の出力を一致させる

症状 原因候補 最初に試すこと
時刻表示だけ一致しない Date.now() / new Date() / locale差 初回値を固定してサーバーから渡す
毎回違うIDでMismatch Math.random()でID生成 useIdまたはデータ由来の安定IDへ変更
window/localStorage参照でMismatch ブラウザ専用値をrender中に参照 初回は共通値を描画しuseEffect後に更新
1要素だけ意図的に時刻が違う 不可避な差分 suppressHydrationWarningを限定的に使う

Reactの`hydrateRoot`は、サーバーで生成済みのHTMLへReactの処理を接続します。React公式は、hydration時のComponent treeがサーバー出力と同じ結果を生成する必要があり、Mismatchはバグとして扱うよう案内しています。


原因1:Date.now()やnew Date()をrender中に計算している

サーバーがHTMLを作る時点と、ブラウザがhydrationする時点は同じ瞬間ではありません。そのため、render中に現在時刻を計算すると、数ミリ秒の差でも文字列が変わる可能性があります。Next.js公式もtime-dependent APIをHydration Errorの代表原因として挙げています。

export default function Clock() {
  return <p>{Date.now()}</p>
}

この書き方ではサーバーとクライアントが別々に`Date.now()`を評価します。表示したい時刻がサーバー基準でよいなら、サーバー側で一度だけ値を作ってClient Componentへ渡し、初回renderではその値を使う方が安定します。

// Server Component
import Clock from './Clock'

export default function Page() {
  const initialTime = Date.now()
  return <Clock initialTime={initialTime} />
}
'use client'

export default function Clock({ initialTime }: { initialTime: number }) {
  return <p>{new Date(initialTime).toISOString()}</p>
}

原因2:Math.random()で表示値やIDを作っている

`Math.random()`も呼ぶたびに結果が変わるため、サーバーとクライアントで同じ値になる保証がありません。特にJSX内でIDや表示順を作ると、Hydration Mismatchの原因を作りやすくなります。

export default function Field() {
  const id = `field-${Math.random()}`
  return <input id={id} />
}

アクセシビリティ属性などのComponent固有IDなら、Reactの`useId`が適しています。React公式は`useId`をserver renderingでも整合するID用途として説明しています。ただし、listの`key`は`useId`ではなくデータ由来の安定したIDを使います。

'use client'

import { useId } from 'react'

export default function Field() {
  const id = useId()
  return <input id={id} aria-describedby={`${id}-help`} />
}

クライアントでしか分からない値はuseEffect後に更新する

`window`、`localStorage`、ユーザーのローカル時刻など、ブラウザで初めて確定する値を表示したい場合があります。このときはサーバーとクライアントの初回renderで同じPlaceholderを返し、hydration後にEffectで更新するとMismatchを避けられます。

'use client'

import { useEffect, useState } from 'react'

export default function LocalClock() {
  const [text, setText] = useState('読み込み中')

  useEffect(() => {
    setText(new Date().toLocaleString())
  }, [])

  return <p>{text}</p>
}

この方法は初回renderを一致させられますが、hydration直後にもう一度renderするため、何でもEffectへ移せばよいわけではありません。サーバーで確定できる値はサーバーから渡し、ブラウザ固有の値だけをEffectへ寄せるのが整理しやすいです。

suppressHydrationWarningは修正ではなくEscape Hatch

時刻のように、1要素だけサーバーとクライアントで違うことが意図されている場合は、Reactの`suppressHydrationWarning`を使えます。

export default function Timestamp() {
  return (
    <time suppressHydrationWarning>
      {new Date().toLocaleString()}
    </time>
  )
}

ただしReact公式では、この指定は1階層だけに効くEscape Hatchと位置付けられています。Mismatchの原因を隠すために広く付けるのではなく、差分が本当に不可避な箇所だけに限定します。

切り分けは「最初のrenderで変わる値」を探す

  • エラーのComponent周辺でDate.now()・new Date()・Math.random()を探す
  • typeof window、localStorage、matchMediaなどブラウザ専用値をrender中に読んでいないか確認する
  • サーバーとクライアントでlocaleやtimeZoneが変わるformat処理を確認する
  • randomなIDはuseIdまたはデータ由来の安定IDへ置き換える
  • サーバーで確定できる値はpropとして渡し、初回renderを一致させる
  • 不可避な1要素だけsuppressHydrationWarningを検討する

「SSRを全部無効にする」前に、どの値が初回renderを変えているかを特定する方が原因を理解しやすく、SSRのメリットも残しやすくなります。

既存のApp Router記事とは役割を分ける

Route ZeroにはApp RouterのSSR・SSG・ISR比較やAPI連携の記事があります。本記事はレンダリング方式の選び方ではなく、Hydration Mismatchという特定エラーの診断へ絞っています。

(SSR・SSG・ISRの選び方を確認したい場合については『Next.js App Router徹底比較:SSR・SSG・ISRの違いと選び方』をご参照ください)

(App RouterのAPI連携パターンを確認したい場合については『Next.js App Router API連携4パターン徹底比較|最適な選び方は?』をご参照ください)

よくある質問

Dateを使うと必ずHydration Mismatchになりますか?

必ずではありません。サーバーとクライアントの初回renderで同じ値・同じformat結果になるならMismatchは起きません。問題は、renderのたびに変わる現在時刻やlocale差などによって初回出力が一致しないことです。

Math.random()はuseEffectへ移せばよいですか?

表示上のrandom値をクライアントで後から決める用途なら選択肢になります。ただしID用途ならReactのuseId、listのkeyならデータ由来の安定IDを使う方が目的に合います。

suppressHydrationWarningを付ければエラーは直りますか?

警告を抑えるためのEscape Hatchであり、根本原因の修正ではありません。React公式も限定的な利用を案内しています。まず初回renderが一致しない理由を直します。

まずMismatchが出ているComponentで、`Date.now()`・`new Date()`・`Math.random()`をrender中に直接呼んでいないか確認してみてください。

カジュアル面談はこちら

RANKINGranking-icon

すべてを開示している会社です

ポジティブな面もネガティブな面も、包み隠さず誠実にお答えします。
気になることがあれば、面談の際にすべてオープンにお話します。