- React
React useIdの使い方 labelとinputのid重複を防ぐ
useIdとは、コンポーネントごとに重複しないid文字列を作ってくれるReactのフックです。 label の htmlFor と input の id を結び付けるときや、aria-describedby で説明文を指すときに使います。同じ部品を画面に何個置いても id がぶつからず、サーバーで作った HTML とブラウザ側の描画(ハイドレーション)でも同じ値になります。
この記事では、同じ入力部品を2回置いたときに id が重複する HTML と、useId で直した HTML を renderToString の出力で比べます。 さらに、Math.random や連番カウンタで id を作るとハイドレーションで警告が出る様子を再現し、useId なら一致することを確かめました。生成される id の形は React のバージョンで変わるため、18.3.1・19.0.0・19.1.0・19.2.0・19.3.0 の5つで実際の値を並べています。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / React・react-dom 19.3.0(形式の比較に 18.3.1・19.0.0・19.1.0・19.2.0)/ esbuild 0.28.2 でビルド / Playwright 1.56.0 の Chromium 141 で実行。
useIdの早見表
| やりたいこと | 書き方 | 補足 |
|---|---|---|
| label と input を結び付ける | const id = useId() → htmlFor={id} と id={id} |
部品を何個置いても id が重ならない |
| 1つの部品に入力が複数ある | id + '-first'、id + '-last' のように接尾辞を付ける |
useId の呼び出しは1回でよい |
| 説明文やエラー文を入力欄に結び付ける | aria-describedby={id + '-hint'} と、説明文側の id={id + '-hint'} |
入力欄の補足説明として関連付けられる |
| 1ページに React アプリを複数載せる | createRoot / hydrateRoot / サーバー側 API に identifierPrefix を渡す |
サーバーとクライアントで同じ値にする |
| リストの key を作る | useId は使わず、データが持つ ID を使う | 同じ値になり警告が出る(後述) |
useId が向く場面と、別の方法を選ぶ場面です。
| 用途 | useId | 代わりに使うもの |
|---|---|---|
htmlFor と id の組 |
向いている | ― |
aria-describedby などの参照先 |
向いている | ― |
リストの key |
向かない | データの ID(task.taskId など) |
| CSS のセレクタ、テスト用の目印 | 向かない | className、data-testid などの固定の名前 |
| データベースに保存する ID | 向かない | サーバー側で採番した値 |
実際に生成された id の形式(詳しくは後述)です。
| React | クライアントだけで描画 | サーバーで描画 |
|---|---|---|
| 18.3.1 / 19.0.0 | :r0: |
:R1: |
| 19.1.0 | «r0» |
«R1» |
| 19.2.0 / 19.3.0 | _r_0_ |
_R_1_ |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「React useIdの使い方 labelとinputのid重複を防ぐ」
準備:renderToStringとハイドレーションを試せる最小構成
サーバーで HTML を作る処理と、ブラウザでハイドレーションする処理を、esbuild でそれぞれ1ファイルにまとめて動かします。
mkdir useid-demo && cd useid-demo
npm init -y
npm i react react-dom
npm i -D esbuild typescript @types/react @types/react-dom
useid-demo/
├─ make-page.mjs … HTML とスクリプトを1ページにまとめる
└─ src/
├─ UseIdField.tsx … 入力部品(以降、版ごとに差し替える)
├─ App.tsx … 部品を2回置く
├─ ssr.tsx … renderToString で HTML を出力
└─ client.tsx … hydrateRoot でハイドレーション
// src/App.tsx
import { UseIdField } from './UseIdField'
export function App() {
return (
<form>
<UseIdField label="名前" />
<UseIdField label="担当者名" />
</form>
)
}
// src/ssr.tsx
import { renderToString } from 'react-dom/server'
import { App } from './App'
// 第2引数の identifierPrefix は省略できる
const prefix = process.argv[2]
const html = renderToString(<App />, prefix ? { identifierPrefix: prefix } : undefined)
console.log(html)
// src/client.tsx
import { hydrateRoot } from 'react-dom/client'
import { App } from './App'
// サーバー側と同じ identifierPrefix を渡す
const prefix = document.body.dataset.prefix
hydrateRoot(document.getElementById('root')!, <App />, prefix ? { identifierPrefix: prefix } : undefined)
// make-page.mjs
import { execFileSync } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
// サーバーで作った HTML を root に入れ、クライアント用のスクリプトを読み込むページを作る
const html = execFileSync('node', ['out/ssr.cjs']).toString().trim()
const js = readFileSync('out/client.js', 'utf8')
writeFileSync(
'out/index.html',
`<!doctype html><meta charset="utf-8"><body><div id="root">${html}</div><script>${js}</script></body>`,
)
ビルドと実行のコマンドです。警告を見たいので、クライアント側は開発ビルドにしています。
npx esbuild src/ssr.tsx --bundle --platform=node --jsx=automatic --outfile=out/ssr.cjs
npx esbuild src/client.tsx --bundle --jsx=automatic '--define:process.env.NODE_ENV="development"' --outfile=out/client.js
node out/ssr.cjs # HTML を表示
node make-page.mjs # out/index.html を作る(ブラウザで開く)
以降に載せる HTML は、読みやすいようにタグの間で改行しています。
idを固定で書いた部品を2回置くと重複する
まず、id を固定の文字列で書いた入力部品です。1個だけ置くぶんには問題なく動きます。
// src/FixedIdField.tsx
// id を固定の文字列で書いた入力部品
export function FixedIdField({ label }: { label: string }) {
return (
<div>
<label htmlFor="name">{label}</label>
<input id="name" type="text" />
</div>
)
}
これを「名前」「担当者名」の2か所で使い、renderToString で HTML にした結果です。
<form>
<div>
<label for="name">名前</label>
<input id="name" type="text"/>
</div>
<div>
<label for="name">担当者名</label>
<input id="name" type="text"/>
</div>
</form>
id="name" が2つ、for="name" も2つあります。 HTML では id はページ内で1つだけという決まりなので、ブラウザは最初に見つかった要素を採用します。このページを Chromium で開き、2つ目のラベル(担当者名)をクリックした結果です。
ハイドレーション後の input の id: ["name","name"]
2つ目のラベルをクリック → フォーカスが当たった input: 1 番目
同じ id を持つ要素の数: {"root":1,"name":2}
「担当者名」をクリックしたのに、フォーカスは1番目(名前)の入力欄に移りました。 エラーも警告も出ないまま、ラベルと入力欄の対応がずれています。
props で id を渡してもらう方法でも直せますが、使う側が毎回ぶつからない名前を考える必要があります。そこで useId を使います。
useIdで直す:部品ごとに別のidが付く
// src/UseIdField.tsx
import { useId } from 'react'
// useId で id を作る入力部品
export function UseIdField({ label }: { label: string }) {
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
変更点は、const id = useId() を足して、固定の文字列を {id} に置き換えただけです。引数はありません。renderToString の出力です。
<form>
<div>
<label for="_R_1_">名前</label>
<input id="_R_1_" type="text"/>
</div>
<div>
<label for="_R_2_">担当者名</label>
<input id="_R_2_" type="text"/>
</div>
</form>
同じ操作をした結果です。
ハイドレーション後の input の id: ["_R_1_","_R_2_"]
2つ目のラベルをクリック → フォーカスが当たった input: 2 番目
同じ id を持つ要素の数: {"root":1,"_R_1_":1,"_R_2_":1}
1つ目は _R_1_、2つ目は _R_2_ と別の id になり、2つ目のラベルで2番目の入力欄にフォーカスが移りました。 部品を使う側は id を意識する必要がありません。
直す前と後の比較
| 項目 | 固定の id | useId |
|---|---|---|
| 部品を2回置いたときの id | name と name(重複) |
_R_1_ と _R_2_ |
| 2つ目のラベルをクリック | 1番目の入力欄にフォーカス | 2番目の入力欄にフォーカス |
| 使う側が id を考える必要 | あり | なし |
| コンソールの警告 | なし(気づきにくい) | なし |
生成されるidの形式はバージョンで違う
useId が返す文字列の形は、React のバージョンによって変わります。同じコードを5つのバージョンで動かし、クライアントだけで描画した場合(createRoot)とサーバーで描画した場合(renderToString)の値を調べました。
| React | createRoot での1個目・2個目 | renderToString での1個目・2個目 | 同じ部品で2回呼んだとき(サーバー) | identifierPrefix に app1- |
|---|---|---|---|---|
| 18.3.1 | :r0: :r1: |
:R1: :R2: |
:R0: :R0H1: |
:app1-R1: |
| 19.0.0 | :r0: :r1: |
:R1: :R2: |
:R0: :R0H1: |
:app1-R1: |
| 19.1.0 | «r0» «r1» |
«R1» «R2» |
«R0» «R0H1» |
«app1-R1» |
| 19.2.0 | _r_0_ _r_1_ |
_R_1_ _R_2_ |
_R_0_ _R_0H1_ |
_app1-R_1_ |
| 19.3.0 | _r_0_ _r_1_ |
_R_1_ _R_2_ |
_R_0_ _R_0H1_ |
_app1-R_1_ |
読み取れることを整理します。
- 18.3.1 と 19.0.0 はコロンで囲んだ形、19.1.0 は二重山かっこ(« »)、19.2.0 以降は下線を使った形でした
- クライアントだけで描画すると小文字の r と連番、サーバーで描画すると大文字の R になります。サーバー側の値は、コンポーネントが木構造のどこにあるかから決まります
- 1つのコンポーネントで useId を2回呼ぶと、2個目には
H1が付いた別の値が返ります - identifierPrefix は、囲みの記号のすぐ内側に入ります
このように形式は変わることがあるので、「id の中身がこの形である」ことに頼った処理は書かないのが安全です。文字列を分解したり、特定の値と比較したりせず、受け取った値をそのまま属性に渡す使い方にとどめます。
2回呼んだときのコードと出力
// src/TwoIds.tsx
import { useId } from 'react'
// 1つのコンポーネントで useId を2回呼ぶ
export function TwoIds() {
const a = useId()
const b = useId()
return <p data-a={a} data-b={b}>two ids</p>
}
<p data-a="_R_0_" data-b="_R_0H1_">two ids</p>
2回呼んでもかまいませんが、次の章の接尾辞を使えば1回で足ります。
1つのidに接尾辞を付けて複数の入力とaria-describedbyに使う
1つの部品に入力欄がいくつもあるとき、入力欄の数だけ useId を呼ぶ必要はありません。useId は1回だけ呼び、接尾辞を足して使い分けます。 説明文を入力欄に結び付ける aria-describedby にも同じ方法が使えます。
// src/ProfileFields.tsx
import { useId } from 'react'
// 1つの id に接尾辞を付けて、複数の入力と説明文に使う
export function ProfileFields() {
const id = useId()
return (
<fieldset>
<legend>会員情報</legend>
<label htmlFor={id + '-last'}>姓</label>
<input id={id + '-last'} type="text" />
<label htmlFor={id + '-first'}>名</label>
<input id={id + '-first'} type="text" />
<label htmlFor={id + '-pw'}>パスワード</label>
<input id={id + '-pw'} type="password" aria-describedby={id + '-pw-hint'} />
<p id={id + '-pw-hint'}>8文字以上で入力してください</p>
</fieldset>
)
}
renderToString の出力です。
<fieldset>
<legend>会員情報</legend>
<label for="_R_0_-last">姓</label>
<input id="_R_0_-last" type="text"/>
<label for="_R_0_-first">名</label>
<input id="_R_0_-first" type="text"/>
<label for="_R_0_-pw">パスワード</label>
<input id="_R_0_-pw" type="password" aria-describedby="_R_0_-pw-hint"/>
<p id="_R_0_-pw-hint">8文字以上で入力してください</p>
</fieldset>
元の id(_R_0_)が部品ごとに違うので、接尾辞を付けた id も部品ごとに違う値になります。この部品を何個置いても重なりません。
| 属性 | 役割 | この例での値 |
|---|---|---|
htmlFor(HTML では for) |
ラベルが指す入力欄の id | _R_0_-pw |
id |
要素の名前 | _R_0_-pw、_R_0_-pw-hint |
aria-describedby |
入力欄の補足説明がある要素の id | _R_0_-pw-hint |
Math.randomや連番でidを作るとハイドレーションで警告が出る
「重ならない id が欲しいだけなら、乱数や連番でも作れるのでは」と考えるかもしれません。ブラウザだけで描画するアプリなら動きますが、サーバーで HTML を作ってブラウザでハイドレーションする構成では、サーバーとブラウザで値が食い違います。 実際に確かめます。
Math.randomで作った場合
// src/RandomIdField.tsx
import { useState } from 'react'
// Math.random で id を作る入力部品
export function RandomIdField({ label }: { label: string }) {
const [id] = useState(() => 'f-' + Math.random().toString(36).slice(2, 8))
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
サーバーが出力した HTML です。
<form>
<div>
<label for="f-ngbs4e">名前</label>
<input id="f-ngbs4e" type="text"/>
</div>
<div>
<label for="f-ckbb43">担当者名</label>
<input id="f-ckbb43" type="text"/>
</div>
</form>
この HTML をブラウザでハイドレーションしたときのコンソール出力(抜粋)です。
[console.error] A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot of it along with the HTML.
- Invalid HTML tag nesting.
<random>
<form>
<RandomIdField label="名前">
<div>
<label
+ htmlFor="f-gblu5b"
- htmlFor="f-ngbs4e"
>
<input
+ id="f-gblu5b"
- id="f-ngbs4e"
type="text"
>
ハイドレーション後の input の id: ["f-ngbs4e","f-ckbb43"]
+ がブラウザ側で計算した値、- がサーバーの HTML にあった値です。乱数は呼ぶたびに変わるので、サーバーでは f-ngbs4e、ブラウザでは f-gblu5b と別の値になりました。メッセージの原因一覧にも Math.random が挙がっています。
メッセージに「This won't be patched up」とあるとおり、ハイドレーション後も DOM の id はサーバーの値(f-ngbs4e)のままでした。React が持っている値と画面の値がずれた状態になります。
連番カウンタで作った場合
// src/CounterIdField.tsx
import { useState } from 'react'
let counter = 0
// モジュール変数の連番で id を作る入力部品
export function CounterIdField({ label }: { label: string }) {
const [id] = useState(() => 'f-' + ++counter)
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
)
}
連番は、最初の1回だけ見ると一致します。サーバーの1回目の描画は f-1 と f-2、ブラウザも f-1 と f-2 になり、警告は出ませんでした。
== 1回目のリクエストの HTML をハイドレーション
ハイドレーション後の input の id: ["f-1","f-2"]
ところが、サーバーのプロセスは動き続けるので、カウンタは増え続けます。同じサーバーで2回目に描画した HTML は f-3 と f-4 になり、ブラウザ側(毎回 1 から数える)と食い違います。
<form>
<div>
<label for="f-3">名前</label>
<input id="f-3" type="text"/>
</div>
<div>
<label for="f-4">担当者名</label>
<input id="f-4" type="text"/>
</div>
</form>
== 2回目のリクエストの HTML をハイドレーション
[console.error] A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. …
<CounterIdField label="名前">
<div>
<label
+ htmlFor="f-1"
- htmlFor="f-3"
>
<input
+ id="f-1"
- id="f-3"
type="text"
>
<CounterIdField label="担当者名">
<div>
<label
+ htmlFor="f-2"
- htmlFor="f-4"
>
<input
+ id="f-2"
- id="f-4"
type="text"
>
ハイドレーション後の input の id: ["f-3","f-4"]
手元で1回だけ試したときには気づかず、アクセスが重なってから現れるのが連番の特徴です。公式ドキュメントも、連番カウンタではサーバーが HTML を出力した順序とブラウザがハイドレーションする順序が一致するとは限らず、保証が難しいと説明しています。
useIdならサーバーとブラウザで一致する
同じ手順を useId の部品(UseIdField)で行った結果です。
== useId の HTML をハイドレーション
ハイドレーション後の input の id: ["_R_1_","_R_2_"]
コンソールには何も出ませんでした。サーバーで何回描画しても _R_1_ と _R_2_ のままです。useId の値は、乱数や呼び出し回数ではなくコンポーネントが木構造のどの位置にあるかから作られるので、同じ構造を描画すれば同じ値になります。
| id の作り方 | サーバー1回目 | サーバー2回目 | ブラウザ | ハイドレーションの警告 |
|---|---|---|---|---|
Math.random() |
f-ngbs4e |
f-t0qtkl |
f-gblu5b |
毎回出る |
| 連番カウンタ | f-1 |
f-3 |
f-1 |
2回目以降に出る |
useId() |
_R_1_ |
_R_1_ |
_R_1_ |
出ない |
identifierPrefix:1ページに複数のReactアプリを載せるとき
1つのページに、別々にビルドした React アプリを2つ載せる場合を考えます。それぞれのアプリは自分の中で 0 から数えるので、id が重なります。同じ部品を使う2つのスクリプトを1ページで動かした結果です。
input の id: ["_r_0_","_r_1_","_r_0_","_r_1_"]
_r_0_ と _r_1_ が2組でき、重複しました。 これを防ぐのが identifierPrefix です。createRoot の第2引数に渡すと、そのルートの中で useId が作る id に接頭辞が付きます。
// src/client.tsx(createRoot で2つのルートを作る例)
import { createRoot } from 'react-dom/client'
import { App } from './App'
createRoot(document.getElementById('root1')!, { identifierPrefix: 'app1-' }).render(<App />)
createRoot(document.getElementById('root2')!, { identifierPrefix: 'app2-' }).render(<App />)
input の id: ["_app1-r_0_","_app1-r_1_","_app2-r_2_","_app2-r_3_"]
サーバーで描画する場合は、サーバー側の API とブラウザ側の hydrateRoot の両方に同じ値を渡します。準備の章の ssr.tsx と client.tsx はこの形になっています。3通りの組み合わせで確かめました。
| サーバー側 | ブラウザ側(hydrateRoot) | サーバーの id | 結果 |
|---|---|---|---|
| 指定なし | 指定なし | _R_1_ _R_2_ |
警告なし |
app1- |
app1- |
_app1-R_1_ _app1-R_2_ |
警告なし |
app1- |
指定なし | _app1-R_1_ _app1-R_2_ |
ハイドレーション不一致の警告 |
片方だけに指定したときのコンソール出力(抜粋)です。
[console.error] A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. …
+ htmlFor="_R_1_"
- htmlFor="_app1-R_1_"
+ id="_R_1_"
- id="_app1-R_1_"
| 指定する場所 | 書き方 |
|---|---|
| ブラウザだけで描画 | createRoot(container, { identifierPrefix: 'app1-' }) |
| サーバーで HTML を作る | renderToString(element, { identifierPrefix: 'app1-' }) |
| ハイドレーション | hydrateRoot(container, element, { identifierPrefix: 'app1-' }) |
keyやCSSセレクタに使わない理由
リストのkeyに使うと同じ値になる
useId はフックなので、ループの中では呼べません。そこでコンポーネントの先頭で1回呼んだ値を key に使うと、全部の行が同じ key になります。
// src/KeyList.tsx
import { useId } from 'react'
const tasks = [
{ taskId: 't-101', title: '見積もりを送る' },
{ taskId: 't-102', title: '会議室を予約する' },
]
// useId の値を key に使った例
export function KeyListWithUseId() {
const id = useId()
return (
<ul>
{tasks.map((task) => (
<li key={id}>{task.title}</li>
))}
</ul>
)
}
// データが持つ ID を key に使う
export function KeyListWithData() {
return (
<ul>
{tasks.map((task) => (
<li key={task.taskId}>{task.title}</li>
))}
</ul>
)
}
== KeyListWithUseId
[console.error] Encountered two children with the same key, `_r_0_`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
== KeyListWithData
(コンソール出力なし)
key は「どのデータの行か」を React に伝えるためのものです。公式ドキュメントも、key はデータから作るものとして、useId を key の生成に使わないよう案内しています。key にはデータが持つ ID(この例では taskId)を使います。
CSSセレクタに使うと、バージョンと描画方法で結果が変わる
useId の値を querySelector のセレクタに使えるかを、バージョンごとに試しました。
== React 18.3.1 / 19.0.0(id は :r0:)
querySelector('#:r0:') → SyntaxError: Failed to execute 'querySelector' on 'Document': '#:r0:' is not a valid selector.
querySelector('#' + CSS.escape(id)) → INPUT / CSS.escape(id) = \:r0\:
getElementById(id) → INPUT
== React 19.1.0(id は «r0»)
querySelector('#«r0»') → INPUT
== React 19.2.0 / 19.3.0(id は _r_0_)
querySelector('#_r_0_') → INPUT
getElementById(id) → INPUT
コロンを含む形式では、そのままではセレクタとして解釈できず SyntaxError になります。19.1.0 以降の形式ではセレクタとしても通りました。ただし、通るからといって CSS ファイルに #_r_0_ と書くのは向きません。理由は3つあります。
| 理由 | この記事で確かめた内容 |
|---|---|
| 値がバージョンで変わる | :r0: → «r0» → _r_0_ |
| 描画方法で変わる | クライアントだけなら _r_0_、サーバーで描画すると _R_1_ |
| 部品の位置や数で変わる | 1個目は _R_1_、2個目は _R_2_。部品を足すと番号がずれる |
スタイルやテストの目印には、className や data-testid のような固定の名前を使います。JavaScript から id で要素を探すなら、document.getElementById(id) が今回試したどのバージョンでも動きました。
そのほかの注意点(公式ドキュメントより)
| 注意点 | 内容 |
|---|---|
| 呼べる場所 | コンポーネントか自作フックのトップレベルだけ。ループや条件分岐の中では呼べない |
use() のキャッシュのキー |
生成に使わない。キーはデータから作る |
| async のサーバーコンポーネント | 現時点では useId を使えない |
つまずきやすい点と直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| ラベルをクリックすると別の入力欄にフォーカスが移る | 固定の id が重複している | 部品の中で useId を使う |
| ハイドレーションで id の不一致の警告が出る | 乱数・連番・時刻などで id を作っている | useId に置き換える |
| useId なのに不一致の警告が出る | identifierPrefix がサーバーとブラウザで違う | 両方に同じ値を渡す |
| 複数の React アプリで id が重なる | 別々のアプリがそれぞれ 0 から数えている | アプリごとに identifierPrefix を変える |
| 「Encountered two children with the same key」と出る | useId の値を key に使っている | データの ID を key にする |
querySelector で SyntaxError になる(18.3.1・19.0.0) |
id にコロンが含まれる | getElementById か CSS.escape を使う。目印は固定の名前にする |
よくある質問
Q. 入力欄ごとに useId を呼ぶ必要がありますか?
A. 1つのコンポーネントで1回呼び、id + '-first' のように接尾辞を付ければ足ります。2回呼んでも動き、19.3.0 では _R_0_ と _R_0H1_ のように別の値が返りました。
Q. サーバーで描画しないアプリでも useId を使う意味はありますか? A. あります。部品を何個置いても id が重ならないという利点は、ブラウザだけで描画するアプリでも同じです。乱数や連番を自分で管理する必要がなくなり、あとからサーバーでの描画を取り入れたときもそのまま動きます。
Q. label で input を囲めば、id は要りませんか? A. label の中に input を入れれば、htmlFor と id がなくてもラベルと入力欄は結び付きます(Chromium 141 で、ラベルの文字をクリックすると中の入力欄にフォーカスが移ることを確かめました)。ラベルと入力欄を別の場所に置きたいときや、aria-describedby で説明文を指すときは id が必要になるので、そこで useId が役立ちます。
Q. useId の値を固定の文字列だと思ってテストに書いてよいですか?
A. 向きません。形式はバージョンで変わり(18.3.1 は :r0:、19.3.0 は _r_0_)、部品の位置でも番号が変わります。テストではラベルの文言や role、data-testid で要素を探す書き方が安定します。
まとめ
- useId は引数なしで呼び、返った文字列を
htmlForとidに渡す。 同じ部品を何個置いても id が重ならない - 固定の id を2回置くと、2つ目のラベルが1番目の入力欄を指す。 エラーが出ないので気づきにくい
- Math.random や連番で作った id は、ハイドレーションで不一致の警告が出る。 useId はサーバーとブラウザで同じ値になる
- id の形式はバージョンで変わる。 18.3.1・19.0.0 は
:r0:、19.1.0 は«r0»、19.2.0 以降は_r_0_。中身の形に頼らない - 入力が複数あるときは接尾辞を付ける。 aria-describedby の参照先にも同じ方法が使える
- 複数の React アプリを1ページに載せるときは identifierPrefix。 サーバーとブラウザで同じ値を渡す
- key とCSSセレクタには使わない。 key はデータの ID、目印は固定の名前にする
参考・出典
- React 公式ドキュメント「useId」 https://react.dev/reference/react/useId
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「React useIdの使い方 labelとinputのid重複を防ぐ」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。