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中に直接呼んでいないか確認してみてください。