React 18 以降では、フォーム要素やアクセシビリティ対応のために useId()
というフックが導入されました。これにより、サーバーサイドレンダリング (SSR) とクライアントで同一の一意な ID を生成できます。しかし、条件付きレンダーや配列ループ内で useId()
を呼び出すと、Hydration Mismatch が発生することがあります。
本記事では、実際の再現コード、エラー内容、原因の詳細、そして実用的な回避策を紹介します。
1. Hydration Mismatch の再現例
まずはシンプルな再現コードを見てみましょう。
import React, { useId } from "react";
export default function Sample({ showExtra }) {
const id1 = useId();
let id2;
if (showExtra) {
id2 = useId();
}
return (
<div>
<label htmlFor={id1}>Name</label>
<input id={id1} type="text" />
{showExtra && (
<>
<label htmlFor={id2}>Email</label>
<input id={id2} type="email" />
</>
)}
</div>
);
}
このコンポーネントを SSR + Hydration 環境でレンダリングすると、クライアントでは次のような警告が表示されます。
Warning: Text content does not match server-rendered HTML.
また、input
の id
がサーバー側とクライアント側で異なり、ラベルの関連付けが崩れる可能性があります。
2. 原因の詳細
useId()
は 呼び出し順序と呼び出し回数に基づいて ID を生成 します。SSR とクライアントの間で呼び出し回数が一致しないと、異なるシーケンスの ID が割り当てられます。
条件付きレンダーの中で useId()
を呼ぶと、サーバー側では showExtra = false
だったがクライアント側では true
だった、というケースで呼び出し数がずれ、Hydration Mismatch が発生します。同様に、配列ループの中で useId()
を使う場合も、要素数が一致しないと同じ問題が起こります。
3. よくある間違いパターン
- 条件付きレンダーで直接 useId() を呼ぶ
- map() 内で useId() を呼ぶが、要素数が SSR と CSR で異なる
- SSR と CSR で初期 state が異なる
これらのケースでは、サーバーとクライアントで useId()
の呼び出し回数が一致しません。
4. 解決策1: useId を条件外で呼び出す
基本的な回避方法は「常に同じ回数 useId()
を呼ぶ」ことです。条件付きレンダーの外で呼び出しておき、必要に応じて利用します。
const id1 = useId();
const id2 = useId(); // 常に呼び出す
return (
<div>
<label htmlFor={id1}>Name</label>
<input id={id1} type="text" />
{showExtra && (
<>
<label htmlFor={id2}>Email</label>
<input id={id2} type="email" />
</>
)}
</div>
);
これにより SSR と CSR で呼び出し回数が一致し、Hydration Mismatch を防げます。
5. 解決策2: ループではキーと組み合わせる
配列ループ内では useId()
の呼び出し回数が変化しないよう、ループの外で必要な ID をあらかじめ作成するか、安定したキーと組み合わせます。
const ids = Array.from({ length: items.length }, () => useId());
return (
<ul>
{items.map((item, i) => (
<li key={item.id}>
<label htmlFor={ids[i]}>{item.name}</label>
<input id={ids[i]} />
</li>
))}
</ul>
);
これにより、要素数が一致する限り SSR と CSR で同じ ID が生成されます。
6. まとめ
React 18 の useId()
は便利ですが、呼び出し順序が変わると SSR と CSR の間で ID がずれて Hydration Mismatch が発生 します。常に同じ回数 useId()
を呼び出すよう設計し、条件付きレンダーや配列ループの外で呼び出しておくのがベストプラクティスです。
- 条件分岐の外で useId() を呼び出す
- 配列ループでは安定したキーとともに useId() を使う
- SSR と CSR で初期 state が一致するように注意する
これらを徹底すれば、ラベルと入力要素の関連付けが崩れたり、Hydration エラーで悩まされたりすることがなくなります。
参考
- React Docs – useId [https://react.dev/reference/react/useId]
- React Docs – Hydration Mismatch [https://react.dev/reference/react-dom/client/hydrateRoot]
- RFC: React 18 SSR Improvements [https://github.com/reactjs/rfcs/pull/189]