メインコンテンツへスキップ

useId 完全入門【React】アクセシビリティのためのユニークID生成

ReactのuseIdとは何か、フォームのラベルとinputを正しく紐付けるためのユニークID生成の仕組みを解説。サーバーサイドレンダリングでのハイドレーション問題の解決方法も紹介します。

#react#hooks#useid#javascript#frontend

「フォームの labelinput を紐付けるとき、IDをどう決めればいい?」——Reactでコンポーネントを作っていると、ユニークなIDが必要な場面によく遭遇します。

useId はこの問題を解決するためのフックです。サーバーとクライアントで一致するユニークなIDを自動生成してくれます。

この記事でわかること:

  • useId が必要な理由(なぜ Math.random() や連番はダメなのか)
  • 基本的な使い方
  • 複数のID要素への応用
  • アクセシビリティ(aria-*)との組み合わせ
  • リストのキーには使ってはいけない理由
  • Next.js での SSR 環境での活用方法

useId とは?

useId は、アクセシビリティ属性などに使えるユニークなIDを生成するフックです。

const id = useId();

パラメータはありません。戻り値は :r0:, :r1: のような形式のユニークな文字列です。

なぜ useId が必要なのか?

問題1:Math.random() を使うとSSRで壊れる

// ❌ NG:サーバーとクライアントで異なるIDが生成される
function EmailField() {
  const id = Math.random(); // サーバー: 0.123... クライアント: 0.456...
 
  return (
    <>
      <label htmlFor={id}>メールアドレス</label>
      <input id={id} type="email" />
    </>
  );
}

Next.js などの SSR 環境では、サーバーで生成したIDとクライアントで生成したIDが異なるとハイドレーションエラーが発生します。ハイドレーションとは、サーバーで生成したHTMLにクライアント側のReactが「命を吹き込む」プロセスです。この2つでIDが一致しないと、Reactが「あれ?サーバーとクライアントで構造が違う」と判断してエラーになります。

問題2:グローバルカウンタはコンポーネントの再利用に弱い

// ❌ NG:同じコンポーネントを複数使うと ID が重複することがある
let counter = 0;
function EmailField() {
  const id = `email-field-${counter++}`; // コンテキストによって不安定
}

useId を使うと、コンポーネントごと・フック呼び出しごとに安定したユニークIDが保証されます。

基本的な使い方

import { useId } from "react";
 
function EmailField() {
  const id = useId();
 
  return (
    <div>
      <label htmlFor={id}>メールアドレス</label>
      <input
        id={id}
        type="email"
        placeholder="example@mail.com"
      />
    </div>
  );
}

labelhtmlForinputid を同じ値にすることで、クリックするとフォームにフォーカスが当たるアクセシブルなフォームが作れます。

なぜ labelinput を紐付けることが重要なのか?

  • スクリーンリーダー(視覚障害者向けの読み上げソフト)が「このinputは何を入力するものか」を正確に読み上げられる
  • ラベルをクリックするとinputにフォーカスが当たり、操作しやすくなる
  • WAI-ARIA ガイドラインに準拠したアクセシブルなフォームになる

複数の要素に使う場合

一つのコンポーネントに複数のIDが必要なとき、同じ useId の戻り値をプレフィックスとして使うのが効率的です。

import { useId } from "react";
 
function PasswordField() {
  const id = useId();
 
  return (
    <div>
      <label htmlFor={`${id}-password`}>パスワード</label>
      <input
        id={`${id}-password`}
        type="password"
        aria-describedby={`${id}-hint`}
      />
      <p id={`${id}-hint`}>
        8文字以上の英数字を入力してください。
      </p>
    </div>
  );
}

useId を複数回呼び出すより、1回の戻り値にサフィックスを付ける方がシンプルです。

アクセシビリティへの応用

useIdaria-* 属性との組み合わせにも便利です。

import { useId } from "react";
 
function FormField({ label, type = "text", hint, required = false }) {
  const id = useId();
 
  return (
    <div>
      <label htmlFor={id}>
        {label}
        {required && <span aria-hidden="true"> *</span>}
      </label>
      <input
        id={id}
        type={type}
        required={required}
        aria-required={required}
        aria-describedby={hint ? `${id}-hint` : undefined}
      />
      {hint && (
        <p id={`${id}-hint`} style={{ fontSize: "0.8em", color: "gray" }}>
          {hint}
        </p>
      )}
    </div>
  );
}
 
// 使用例:同じコンポーネントを複数使っても ID は重複しない
function ContactForm() {
  return (
    <form>
      <FormField
        label="氏名"
        required
        hint="フルネームで入力してください"
      />
      <FormField
        label="電話番号"
        type="tel"
        hint="ハイフンなしで入力(例:09012345678)"
      />
      <FormField label="メッセージ" />
    </form>
  );
}

このように FormField を何度使っても、それぞれ独立したユニークIDが割り当てられます。

より複雑なフォームへの応用

チェックボックスグループやラジオボタンのグループにも活用できます。

import { useId } from "react";
 
function CheckboxGroup({ legend, options, name }) {
  const groupId = useId();
 
  return (
    <fieldset aria-labelledby={`${groupId}-legend`}>
      <legend id={`${groupId}-legend`}>{legend}</legend>
      {options.map((option, index) => {
        const optionId = `${groupId}-option-${index}`;
        return (
          <div key={option.value}>
            <input
              type="checkbox"
              id={optionId}
              name={name}
              value={option.value}
            />
            <label htmlFor={optionId}>{option.label}</label>
          </div>
        );
      })}
    </fieldset>
  );
}
 
// 使用例
function SkillsForm() {
  return (
    <CheckboxGroup
      legend="得意な技術を選んでください"
      name="skills"
      options={[
        { value: "react", label: "React" },
        { value: "typescript", label: "TypeScript" },
        { value: "nextjs", label: "Next.js" },
      ]}
    />
  );
}

Next.js での SSR 環境での注意点

Next.js のような SSR 環境では useId が特に重要です。サーバーとクライアントで同じIDが生成されるため、ハイドレーションエラーなしにアクセシブルなフォームを作れます。

// Next.js の Server Component でも問題なく使える
import { useId } from "react";
 
// ただし useId は Client Component でのみ使用可能
"use client";
 
export function LoginForm() {
  const emailId = useId();
  const passwordId = useId();
 
  return (
    <form>
      <div>
        <label htmlFor={emailId}>メールアドレス</label>
        <input id={emailId} type="email" name="email" />
      </div>
      <div>
        <label htmlFor={passwordId}>パスワード</label>
        <input id={passwordId} type="password" name="password" />
      </div>
      <button type="submit">ログイン</button>
    </form>
  );
}

リストのキーには使ってはいけない

// ❌ NG:useId はリストのキーには使えない
function List({ items }) {
  return (
    <ul>
      {items.map((item) => {
        const id = useId(); // ← Hooksのルール違反(ループ内で使っている)
        return <li key={id}>{item.name}</li>;
      })}
    </ul>
  );
}
 
// ✅ OK:リストのキーはデータ由来のIDを使う
function List({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

フックはループや条件分岐の中では使えません(React Hooks のルール)。また、リストのキーはデータに含まれる実際のIDを使うべきです。リストのキーは要素の順序変化を追跡するためのもので、useId で生成した値は適していません。

まとめ

  • useId はアクセシビリティ属性に使えるユニークなIDを生成するフック
  • サーバーとクライアントで一致するIDを生成するため、SSR 環境でも安全
  • htmlForidaria-describedby など、要素同士を紐付けるのに使う
  • 複数のIDが必要な場合は1つの useId にサフィックスを付けて使う
  • リストのキーには使ってはいけない(データ由来のIDを使う)
  • アクセシブルなフォームを作るうえで欠かせないフック

PR

useId 公式ドキュメントで詳細を確認しよう

useIdのインタラクティブなサンプルや、複数のReactアプリが同じページに存在する場合の設定は公式ドキュメントで確認できます。

useId 公式ドキュメントを見る

PR

関連記事