Hydration とは何か
Hydration は、サーバーが送った HTML に、ブラウザ側の React を接続して操作できるようにする処理です。
#なぜ二度描くのか
サーバーが HTML を送るだけなら、画面はすぐ出ます。ただし、そのままではボタンを押しても何も起きません。イベントを繋ぐ JavaScript が動いていないからです。
そこで、ブラウザ側でも同じ内容をもう一度描いて、送られてきた HTML に React を接続します。これが hydration です。
1. サーバーで描く ──▶ HTML 「見える」だけの状態
2. JavaScript が届く
3. ブラウザで描き直す ──▶ 突き合わせて接続 「押せる」状態になる
乾いた HTML に水を差して動くようにする、という比喩から来た名前です。
#対象は Client Component だけ
ここを取り違えると、原因の見当がずれます。
| サーバーで実行 | ブラウザで実行 | hydration の対象 | |
|---|---|---|---|
| Server Component | する | しない | 対象外 |
| Client Component | する(最初の1回) | する | 対象 |
Client Component は「ブラウザだけで動く」ものではありません。「ブラウザにも送られる」ものです。最初の1回はサーバーでも実行されます。
だから、'use client' を書いたファイルの中で window を直接読むと、その1回目で落ちます。
#食い違うと何が起きるか
サーバーが描いたものと、ブラウザが描いたものが違うと、React は「送られてきた HTML は当てにならない」と判断して、その部分を作り直します。
サーバー <p>2026-08-28T01:40:31.478Z</p>
ブラウザ <p>2026-08-28T01:40:31.676Z</p>
↑ ここが違う → 作り直し
Next.js の開発オーバーレイは、これを差分で見せてくれます。+ がブラウザ側、- がサーバー側です。
ページは動き続けます(開発モードでは Recoverable Error と表示されます)。ただし、
- 作り直しのぶん、表示が一瞬ちらつく
- サーバーで HTML を作った意味が薄れる
ので、直すべき状態です。
#食い違いを生まないための原則
最初の描画では、サーバーとブラウザで必ず同じものを描く。
これだけです。違ってよいのは hydration が終わったあと、つまり 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> // 最初は両方 '—'
}
#食い違いを生みやすいもの
| やっていること | なぜ食い違うか |
|---|---|
Date.now() new Date() を描画で使う | 呼ぶ瞬間が違う |
Math.random() | 毎回変わる |
typeof window で分岐 | サーバーとブラウザで必ず別の枝を通る |
toLocaleString() をロケール指定なしで使う | 環境で書式が変わる |
localStorage を初期値に使う | サーバーには存在しない |
<p> の中に <div> を置く | ブラウザが構造を勝手に直す |
最後の1つは見落としやすいところです。HTML として不正な入れ子は、ブラウザが黙って修正するので、React が描いた木と合わなくなります。
#typeof window は解決策ではない
window is not defined を消すために、こう書きたくなります。
const w = typeof window === 'undefined' ? 0 : window.innerWidth
参照エラーは消えます。ですが、サーバーは 0、ブラウザは実際の値を描くので、今度は hydration の不一致になります。 エラーが別の場所へ移っただけです。
React 自身も、エラー文の原因候補の筆頭にこの書き方を挙げています。
#サーバーから値を渡すという手
「ページを作った時刻」を見せたいだけなら、Server Component で決めて渡せば一致します。
// Server Component
const now = new Date().toISOString()
return <Clock now={now} />
サーバーもブラウザも同じ文字列を描くので、食い違いません。「ブラウザで計算する必要が本当にあるか」を先に考えると、多くの場合これで済みます。
#本番と開発で見え方が違う
| 何が出るか | |
|---|---|
| 開発モード | 全文のエラー、原因の候補、差分、該当行 |
| 本番ビルド | Minified React error #418 だけ |
本番で #418 を見たら、同じ条件を開発モードで再現させてから原因を探してください。本番のログを睨んでいても、行番号は出てきません。
#自分のコードに心当たりが無いとき
ブラウザ拡張が HTML を書き換えていることがあります。シークレットウィンドウで再現しないなら、それが原因です。この場合はコード側では直せません。