できない.dev

React のハイドレーションエラー(Hydration mismatch)が解消できない

SSR が出力した HTML とクライアント側の初回レンダリング結果が食い違うと Hydration mismatch が出る。
new Date() や Math.random() の JSX 直書き、window 依存、HTML 入れ子違反、ブラウザ拡張による DOM 書き換えが主因。

#react#hydration#ssr#nextjs#mismatch

要約

Hydration mismatch は サーバ側でレンダリングした HTML と、クライアント側の React の初回描画結果が一致しない ときに出る。new Date() の直書き、window 参照、無効な HTML 入れ子、ブラウザ拡張による DOM 書き換えが主因。初回はサーバと同じ内容を描画し、差異は useEffect 後に反映する のが基本戦略。

よくある原因

  1. 時刻・乱数を JSX に直接書いている: <p>{new Date().toLocaleString()}</p> はサーバ実行時刻とクライアント実行時刻がズレて mismatch する。
  2. window 依存の分岐: typeof window !== 'undefined' ? <A /> : <B /> のような書き方はサーバとクライアントで異なる枝を選ぶため必ず食い違う。
  3. 無効な HTML ネスト: <p> 内の <div><button> 内の <button><a> の入れ子などはブラウザが閉じタグを補ってツリー構造が変わる。
  4. ブラウザ拡張による DOM 改変: Grammarly やパスワード拡張が <body> 属性を書き換えると、最上位ノードで mismatch が起きる。

解決策

1. ランダム値・時刻は useEffect で

function Clock() {
  const [now, setNow] = useState<string | null>(null);
  useEffect(() => {
    setNow(new Date().toLocaleString());
  }, []);
  return <p>{now ?? "..."}</p>; // SSR でも "..." を出す
}

サーバ HTML とクライアント初回描画を 同じ文字列 にしてから、Effect で書き換える。

2. クライアント専用 UI は isClient で守る

const [isClient, setIsClient] = useState(false);
useEffect(() => setIsClient(true), []);
if (!isClient) return null;
return <OnlyOnClient />;

公式の hydrateRoot ドキュメント のとおり、サーバとクライアントが同一木を作る前提に立ち返り、差異は描画後に反映する。

3. 無効な HTML 入れ子を直す

DevTools のコンソールに In HTML, <div> cannot be a descendant of <p>. のように具体的な要素名と場所が出る。
ブラウザのオートクローズで木構造が変わるため、必ず直す。

4. suppressHydrationWarning(限定的)

時刻表示など、本文と乖離が起こりうると承知の上で抑止する場合のみ。

<time suppressHydrationWarning>{new Date().toISOString()}</time>

公式の説明 のとおり、不一致を「無視する」だけで根本解決ではない。
子要素にむやみに広げない。

この記事は役立ちましたか?