useId Hook
useId is a React hook that generates a unique, stable string identifier you can attach to an element for accessibility purposes, such as linking a label to an input or connecting a field to its error message with aria-describedby. It matters because hard-coded IDs break the moment a component is rendered more than once on a page, and hand-rolled random IDs break server-side rendering because the server and client generate different values. useId solves both problems: every call returns an ID that is unique per component instance and identical between the server-rendered HTML and the client’s hydration pass.
Overview / How It Works
Accessible HTML often requires two elements to reference each other by ID. A label needs a htmlFor that matches its input‘s id; an input flagged as invalid needs an aria-describedby that points at the ID of the paragraph explaining the error. In plain HTML you write these IDs by hand, but in a component-based UI that’s dangerous: if the same component renders twice on one page (say, two <SearchBox /> instances), a hard-coded id="search-input" would appear twice in the DOM, which is invalid HTML and confuses screen readers, since htmlFor may bind to the wrong element.
useId fixes this by asking React itself to hand out an ID that is guaranteed unique among all the IDs React has generated in the current render tree. Internally, React tracks the position of each component within the component tree (not within the DOM, but the logical tree that includes context providers, suspense boundaries, and so on) and derives the ID from that position, prefixed with a colon, such as :r0: or :r1:. Because the ID is derived from tree structure rather than a global counter or Math.random(), the server and the client compute the exact same ID for the exact same component instance, so hydration never mismatches.
This matters specifically because of how React’s render cycle works with server-side rendering. On the server, React renders your component tree once to produce HTML. On the client, React “hydrates” that HTML by re-running your components and attaching event listeners, expecting the output to match exactly. If you used Math.random() or an incrementing module-level counter to build IDs, the server’s counter and the client’s counter could easily diverge (extra renders, strict mode double-invocation, multiple roots on one page), producing a hydration mismatch warning and inconsistent accessibility attributes. useId is structural rather than counter-based, so it avoids this entirely — this is the single reason the hook exists.
useId is not meant for generating keys in a list (React already asks for a stable key derived from your data), and it is not meant for generating visual or business-logic values like order numbers or database keys — it exists purely to link DOM elements together for accessibility and to satisfy HTML’s uniqueness requirements.
Syntax
const id = useId();
| Part | Description |
|---|---|
useId |
Imported from react. Takes no arguments. |
| Return value | A unique string such as :r0:. Treat it as an opaque string — never parse it or assume a numeric format. |
| Call site | Must be called at the top level of a function component or a custom hook, exactly like every other hook. |
If a single component needs several related IDs (a label’s id, an error message’s id, a hint’s id), call useId once and derive suffixes from it with a template literal, such as `${id}-hint`. This keeps the related IDs grouped and avoids burning extra hook calls.
Examples
Example 1: Linking a label to an input
import { useId } from "react";
function LabeledInput({ label, type = "text" }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} />
</div>
);
}
export default function App() {
return (
<div>
<LabeledInput label="Username" />
<LabeledInput label="Email" type="email" />
</div>
);
}
This renders two label/input pairs. Because each LabeledInput is a separate instance in the tree, each call to useId returns a different string (for example :r0: for the username field and :r1: for the email field), so the htmlFor/id pairing is correct for both without either component knowing about the other or needing a prop passed in.
Example 2: Multiple related IDs from one call
import { useId } from "react";
function PasswordField() {
const id = useId();
const hintId = `${id}-hint`;
return (
<div>
<label htmlFor={id}>Password</label>
<input id={id} type="password" aria-describedby={hintId} />
<p id={hintId}>Must be at least 8 characters.</p>
</div>
);
}
This renders a password field whose input announces its hint text to assistive technology via aria-describedby. Notice that useId is called only once; the hint’s ID is derived from it with a plain string suffix. This is the recommended pattern whenever one component needs a family of related IDs — it keeps them visibly connected in the source and avoids unnecessary hook calls.
Example 3: Generating IDs for a dynamic group without calling the hook in a loop
import { useId } from "react";
function RadioGroup({ legend, options }) {
const id = useId();
return (
<fieldset>
<legend>{legend}</legend>
{options.map((option, index) => {
const optionId = `${id}-${index}`;
return (
<div key={option.value}>
<input
type="radio"
id={optionId}
name={id}
value={option.value}
/>
<label htmlFor={optionId}>{option.label}</label>
</div>
);
})}
</fieldset>
);
}
export default function App() {
const options = [
{ value: "small", label: "Small" },
{ value: "medium", label: "Medium" },
{ value: "large", label: "Large" },
];
return <RadioGroup legend="Choose a size" options={options} />;
}
This renders a fieldset with three radio buttons, each with its own label. useId is called exactly once at the top of RadioGroup, then the render already has a unique base ID that the .map() call reuses to build option-index-based IDs for each radio input. Note the important distinction: the key prop on the wrapping div uses option.value, the actual data identity, while the DOM id/htmlFor pair uses the generated useId string. Keys and accessibility IDs solve different problems and should never be the same value.
How It Works Step by Step / Under the Hood
- On mount: React walks the component tree during render. When it reaches a component that calls
useId, it computes an ID from that component’s position in the tree (including how deeply nested it is inside providers, suspense boundaries, and sibling components) and returns it. The value is computed fresh on the render, but because the tree position doesn’t change across the mount, it stays stable for the life of that component instance. - On a state update / re-render: Calling
useIdagain on a re-render returns the exact same string as before, because the component’s position in the tree hasn’t moved. This is whyuseIdis safe to reference inhtmlForandaria-*attributes — it never changes out from under the DOM relationship you built. - Server rendering and hydration: When React renders on the server, it produces IDs based on tree position and embeds them in the HTML. When the same tree is hydrated on the client, React recomputes IDs the same way, using the same tree-position algorithm, so the client’s first render matches the server’s HTML exactly — no mismatch warning, no flicker of IDs changing after hydration.
- On unmount: The ID isn’t stored anywhere outside the component’s fiber, so once the component unmounts, its ID is simply gone; there’s no cleanup to perform.
Common Mistakes
Mistake 1: Using useId as a list key
function List({ items }) {
return (
<ul>
{items.map((item) => {
const id = useId(); // ❌ hook called inside a callback passed to map
return <li key={id}>{item.name}</li>;
})}
</ul>
);
}
This breaks the Rules of Hooks: hooks must be called the same number of times, in the same order, on every render, and calling useId inside a .map() callback calls it a variable number of times depending on the array’s length. It also solves the wrong problem — a key must reflect the underlying data’s identity so React can correctly track additions, removals, and reorders. A freshly-generated ID has no relationship to the data and will actually cause every item to lose its identity across re-renders. Use a stable field from the data instead:
function List({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Mistake 2: Calling useId separately for every related field instead of deriving suffixes
function AddressForm() {
const streetId = useId();
const cityId = useId();
const zipId = useId();
// works, but is verbose and easy to forget when adding a new field
return (
<form>
<label htmlFor={streetId}>Street</label>
<input id={streetId} />
<label htmlFor={cityId}>City</label>
<input id={cityId} />
<label htmlFor={zipId}>ZIP</label>
<input id={zipId} />
</form>
);
}
This is not incorrect — it doesn’t break hydration or the Rules of Hooks — but it’s unnecessary and makes it easy to lose track of which IDs belong together. Prefer calling useId once per logical group and deriving the rest with suffixes, as shown in Example 2, so related fields are visually and structurally tied to one base ID.
Best Practices
- Use
useIdforhtmlFor/idpairs,aria-describedby,aria-labelledby, and any other place HTML or ARIA requires an ID reference between two elements. - Call
useIdonce per logical group and build related IDs with string suffixes, rather than calling it repeatedly for every sub-element. - Never use
useIdas akeyin a list — keys must come from stable data identity, not from a freshly generated string. - Treat the returned string as opaque. Don’t parse it, don’t assume it’s numeric, and don’t rely on its exact format (
:r0:) across React versions. - If you need the same ID prefix across many components in an app (for example, a design system shared by multiple apps on one page), pass an
identifierPrefixoption tocreateRoot/hydrateRootto avoid collisions between separate React trees on the same page. - Don’t reach for
useIdfor keys, database IDs, or any value your backend or business logic depends on — it exists solely for client-side accessibility wiring.
Practice Exercises
- Build a
Checkboxcomponent that accepts alabelprop and usesuseIdto correctly associate a checkbox input with its label. Render three instances on the same page and verify (by inspecting the DOM) that each gets a different ID. - Build a
FormFieldcomponent that renders a text input with both a visible label and an error message below it, using oneuseIdcall and a derived suffix foraria-describedbyso the error is announced only when anerrorprop is provided. - Take the
RadioGroupcomponent from Example 3 and extend it into aCheckboxGroupthat renders a variable number of checkboxes from a data array, reusing the same single-useId-plus-suffix pattern for each checkbox’sid/htmlForpair while keepingkeytied to the underlying data.
Summary
useIdgenerates a unique, stable string ID for a component instance, meant for linking DOM elements via accessibility attributes likehtmlForandaria-describedby.- It is SSR-safe: the ID is derived from the component’s position in the tree, so server-rendered HTML and client hydration always agree, unlike random or counter-based IDs.
- The ID stays the same across re-renders of the same component instance and disappears when the component unmounts.
- Call it once per logical group of related fields and derive suffixes with template literals rather than calling it repeatedly.
- Never use it as a list
key— keys need to reflect data identity, and calling any hook inside a loop violates the Rules of Hooks. - Treat the returned value as an opaque string; don’t parse or depend on its exact format.
