In one minute
A <label htmlFor="x"> focuses the input with id="x", and ids must be unique on the page. A reusable Field component can’t hard-code its id (it may render many times) and shouldn’t force every caller to invent one. useId() returns an id that is unique for each component instance and identical on the server and in the browser, so it also works with server rendering, which a counter or Math.random() doesn’t.
You’ll be able to: connect labels and inputs (and aria-* attributes) safely in reusable components.
1function Field({ label, ...props }) {2 return (<div>3 <label htmlFor="review-title">{label}</label>4 <input id="review-title" {...props} />5 </div>)6}- render phase
<input id="review-title" {...props} />Four fields on the page, four inputs with the same id. - event
<label htmlFor="review-title">{label}</label>Click the label “Your name” in the Desk Lamp form. A label focuses the element whoseidmatches itshtmlFor… - …and with duplicate ids the browser picks the first one in the document. Recorded: focus went to the Ceramic Mug form’s Title field.
1function Field({ label, ...props }) {2 const id = useId()3 return (<div>4 <label htmlFor={id}>{label}</label>5 <input id={id} {...props} />6 </div>)7}- render phase
const id = useId()EachFieldinstance gets its own id, based on its place in the tree. Recorded:_r_0_to_r_3_. - event
<label htmlFor={id}>{label}</label>Click “Your name” in the Desk Lamp form. - Recorded: focus went to the Desk Lamp form’s name field, the right one.
Field component (both recorded).The example: two review forms on one product page
Hard-coded id
function FieldHardcoded({ label, ...props }: { label: string } & ComponentProps<'input'>) {
return (
<div>
<label htmlFor="review-title">{label}</label>
<input id="review-title" {...props} />
</div>
)
}
Both review forms render two Fields. The ids in the page:
[
[
"review-title",
"review-title"
],
[
"review-title",
"review-title"
]
]
The recorder clicked the label “Your name” in the Desk Lamp form. What received focus:
{
"form": "Ceramic Mug",
"field": "title"
}
The wrong field in the wrong form: with duplicate ids, the browser uses the first matching element.
useId
function Field({ label, ...props }: { label: string } & ComponentProps<'input'>) {
const generatedId = useId()
const id = props.id ?? generatedId
return (
<div>
<label htmlFor={id}>{label}</label>
<input {...props} id={id} />
</div>
)
}
[
[
"_r_0_",
"_r_1_"
],
[
"_r_2_",
"_r_3_"
]
]
{
"form": "Desk Lamp",
"field": "author"
}
Every field got its own id, and the label focused its own input. A caller can still pass an explicit id; the component only falls back to the generated one.
How it works
- The id comes from the component’s position in the tree, not from a counter. Server and client render the same tree, so they produce the same ids, and hydration matches. A module-level counter can differ between server and client (separate runtimes, different hydration order);
Math.random()always does. - It’s stable for the life of the component: re-renders return the same string. There’s no setter because it never needs to change.
- One call, many ids: derive related ids from it (
${id}-hint,${id}-error) foraria-describedby. - Several React roots on one page can pass
identifierPrefixtocreateRoot/hydrateRoot(and the matching server API) so their ids don’t collide.
Common mistakes
- Using
useIdfor listkeys. Keys must come from the data (Rendering Arrays). - Hard-coded ids in reusable components.
- Generating ids with a counter or
Math.random()in apps that render on the server.
Interview Q&A
What problem does useId solve?
Unique ids for label/input and aria-* pairs inside components that can appear many times. Recorded: with a hard-coded id, clicking a label in the second form focused the first form’s input; with useId (ids _r_0_ … _r_3_) it focused its own.
Why not Math.random() or a counter?
They can produce different values on the server and the client, which breaks hydration. useId derives the id from the component’s place in the tree, which is the same in both.
Can you use useId for list keys?
No. It identifies a component instance’s position, not a data item. Keys must stay with the data as items move.
Why does useId return no setter?
The id is an identity, not data: it’s fixed for the component’s lifetime so htmlFor and id never get out of sync.
Related
Sources
- react.dev:
useId,hydrateRootidentifierPrefix - Topic order inspired by Kent C. Dodds’ EpicReact React Hooks workshop; the example app and code here are this handbook’s own.
Primary source: react.dev/reference/react/useId