Good to knowHooks · 06

The useId Hook

Unique, SSR-safe ids for label/input pairs in components that render many times.

On this page · 7 sections
  1. In one minute
  2. The example: two review forms on one product page
  3. How it works
  4. Common mistakes
  5. Interview Q&A
  6. Related
  7. Sources

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.

id="review-title"
Field.tsx
1function Field({ label, ...props }) {2  return (<div>3    <label htmlFor="review-title">{label}</label>4    <input id="review-title" {...props} />5  </div>)6}
What you see
Ceramic Mug review: Title · Your name
Desk Lamp review: Title · Your name
Focused: –
  1. render phase<input id="review-title" {...props} />Four fields on the page, four inputs with the same id.
  2. event<label htmlFor="review-title">{label}</label>Click the label “Your name” in the Desk Lamp form. A label focuses the element whose id matches its htmlFor…
  3. …and with duplicate ids the browser picks the first one in the document. Recorded: focus went to the Ceramic Mug form’s Title field.

0 / 0
Two review forms on one product page, built from the same 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

Common mistakes

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.

Sources

Primary source: react.dev/reference/react/useId