useId 完全入門【React】アクセシビリティのためのユニークID生成
ReactのuseIdとは何か、フォームのラベルとinputを正しく紐付けるためのユニークID生成の仕組みを解説。サーバーサイドレンダリングでのハイドレーション問題の解決方法も紹介します。
「フォームの label と input を紐付けるとき、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>
);
}label の htmlFor と input の id を同じ値にすることで、クリックするとフォームにフォーカスが当たるアクセシブルなフォームが作れます。
なぜ label と input を紐付けることが重要なのか?
- スクリーンリーダー(視覚障害者向けの読み上げソフト)が「この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回の戻り値にサフィックスを付ける方がシンプルです。
アクセシビリティへの応用
useId は aria-* 属性との組み合わせにも便利です。
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 環境でも安全
htmlFor・id・aria-describedbyなど、要素同士を紐付けるのに使う- 複数のIDが必要な場合は1つの
useIdにサフィックスを付けて使う - リストのキーには使ってはいけない(データ由来のIDを使う)
- アクセシブルなフォームを作るうえで欠かせないフック
PR
useId 公式ドキュメントで詳細を確認しよう
useIdのインタラクティブなサンプルや、複数のReactアプリが同じページに存在する場合の設定は公式ドキュメントで確認できます。
useId 公式ドキュメントを見る →