Hydration failed の直し方
実際に出るエラー
サーバーで描いた HTML と、ブラウザが最初に描いた内容が食い違ったときに出るエラーです。
#何が起きているか
Client Component は、最初の1回だけサーバーでも実行されます。その結果が HTML として送られ、ブラウザ側で React が同じものをもう一度描いて、両者を突き合わせます。この突き合わせが hydration です。
このとき中身が食い違うと、React は「サーバーが送ってきた HTML は信用できない」と判断し、その部分を作り直します。そのときに出るのがこのエラーです。
サーバー <p>2026-08-28T01:40:31.478Z</p> ← HTML として送られる
ブラウザ <p>2026-08-28T01:40:31.676Z</p> ← 描き直したら違った
↑ 食い違い
#致命的ではない、が放置してはいけない
Next.js の開発オーバーレイでは Recoverable Error(回復可能)と表示されます。実際、React はその部分を作り直すのでページは動きます。
ただし、
- 作り直しのぶん、表示が一瞬ちらつく
- サーバーが作った HTML が捨てられるので、最初の表示が速いという利点が消える
- 食い違いの原因が「ブラウザ限定の値を描画時に読んでいる」なら、別の不具合の予兆でもある
ので、直すべきです。
#原因はほぼこの5つ
React 自身がエラー文の中で候補を挙げてくれます。実際に多いのは上の3つです。
1. typeof window で分岐している
'use client'
export function Stored() {
const v = typeof window === 'undefined' ? 'server' : 'browser'
return <p>{v}</p> // サーバーとブラウザで必ず違う
}
window is not defined を消すためにこう書くと、エラーが hydration の不一致に置き換わるだけです。
2. 時刻や乱数を描画に使っている
return <p>{new Date().toISOString()}</p> // 呼ぶたびに変わる
return <p>{Math.random()}</p>
サーバーで描いた瞬間と、ブラウザで描いた瞬間は必ずずれます。
3. 日付や数値の書式が環境で変わる
toLocaleString() は、サーバーとブラウザでロケールやタイムゾーンが違うと別の文字列になります。
4. HTML の入れ子が正しくない
<p> の中に <div> を入れるなど。ブラウザが勝手に構造を直すので、React が描いたものと合わなくなります。
5. ブラウザ拡張が HTML を書き換えている
自分のコードに問題が無いのにこのエラーが出る場合、これを疑います。シークレットウィンドウで開くと再現しないなら、拡張が原因です。
#最短の確認手順
開発モードで開いてください。オーバーレイが食い違った箇所を差分で見せてくれます。
+ 2026-08-28T01:40:31.676Z ← Client(ブラウザが描いたもの)
- 2026-08-28T01:40:31.478Z ← Server(HTML として届いたもの)
components/Clock.tsx (5:10) @ Clock
行番号まで出るので、原因の特定はここでほぼ終わります。
**本番ビルドでは Minified React error #418 としか出ません。**必ず開発モードで再現させてください。
#直し方
推奨:最初の描画では出さず、useEffect の後に出す
'use client'
import { useEffect, useState } from 'react'
export function Clock() {
const [now, setNow] = useState<string | null>(null)
useEffect(() => {
setNow(new Date().toISOString())
}, [])
return <p>{now ?? '—'}</p>
}
サーバーもブラウザも最初は null を描くので、食い違いません。値が入るのは hydration の後です。
値をサーバーから渡す
時刻を「ページを作った時刻」として見せたいなら、Server Component で決めて props で渡します。両方が同じ値を描くので一致します。
// Server Component
const now = new Date().toISOString()
return <Clock now={now} />
書式をサーバーと揃える
toLocaleString() を使うなら、ロケールとタイムゾーンを明示します。
new Intl.DateTimeFormat('ja-JP', { timeZone: 'Asia/Tokyo' }).format(d)
どうしても違ってよい場合
1要素だけなら suppressHydrationWarning で警告を止められます。ただし警告を消すだけで、作り直しは起きます。乱用しないでください。
#やってはいけない直し方
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null
「マウントするまで何も描かない」という形です。エラーは消えますが、
- そのコンポーネントの初期表示が丸ごと無くなる
- サーバーで HTML を作った意味が消える
- 表示が遅れて見える
ので、部分的な回避にしか使えません。食い違う値だけを後から入れるほうが素直です。
#なぜ開発中に気づきにくいのか
気づけます。むしろ開発モードのほうが親切です。問題は逆で、本番でしか出ないケースがあることです。
- 利用者のタイムゾーンやロケールが開発環境と違う
- 利用者のブラウザ拡張が HTML を書き換える
- 本番でだけ通るデータの経路がある
本番で #418 を見たら、まず同じ条件を開発モードで再現させてください。
#再発防止
- 描画の中で「呼ぶたびに変わる値」を使わない
typeof windowで表示を分岐させない- ロケールに依存する書式は、必ずロケールとタイムゾーンを明示する
#関連するエラー
window をサーバー側で参照した場合は、hydration の前に落ちます。
ReferenceError: window is not defined
typeof window での分岐は、このエラーを hydration の不一致に置き換えているだけ、という関係になります。