React の useId() を条件レンダー + ループで使うと Hydration Mismatch が起きる原因と対策

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.

また、inputid がサーバー側とクライアント側で異なり、ラベルの関連付けが崩れる可能性があります。

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 エラーで悩まされたりすることがなくなります。

参考

タイトルとURLをコピーしました