What is the `useId` hook in React and when should it be used?
TL;DR
useId (added in React 18) generates a stable, unique string ID per component instance, per React root. Its main reason for existing is to produce IDs that match between the server-rendered HTML and the client hydration — a plain incrementing counter would produce mismatches. Within a single root the IDs are unique, but two separate roots on the same page can collide unless you set identifierPrefix on createRoot / hydrateRoot. Use it for things like linking <label htmlFor> to <input id>, never as a list key.
import { useId } from 'react';function NameField() {const id = useId();return (<div><label htmlFor={id}>Name:</label><input id={id} type="text" /></div>);}
What is the useId hook in React and when should it be used?
useId creates component-scoped identifiers for rendered relationships that must remain stable across server rendering, hydration, and later renders.
Introduction to useId
The useId hook was added in React 18. It returns a stable string ID tied to the component instance's position in the tree. The ID is the same on every render of that instance, and — crucially — it is the same on the server and the client.
Why useId exists: SSR and hydration
The actual motivation for useId is server-side rendering. Before React 18, generating IDs with a module-level counter (let next = 0; const id = next++;) could cause hydration mismatches: the server and client might increment the counter in a different order, producing different IDs. React reports mismatches and may regenerate the affected tree on the client, depending on where and how the mismatch occurs.
Module-level counters were already fragile before React 18. Any re-render, multiple renderToString calls sharing module state, or a tree shape that differed between server and client could desynchronize the counter. React 18's concurrent rendering and streaming SSR amplified the problem: Suspense boundaries can resolve out of order, the renderer can pause and resume work, and streamed chunks can arrive at the client in a different order than they were rendered on the server. Any ID source that depends on render order (counters, Math.random() seeded once, mutable module state) inherits this fragility.
useId sidesteps it by deriving the ID from the component's location in the React tree, which is identical on the server and the client regardless of render order. Producing IDs unique to one component instance is a side benefit; the main job is hydration stability.
useId vs other ID-generation approaches
useId exists specifically to be SSR-safe and stable across renders. Common alternatives fail one of those two requirements:
| Approach | SSR-safe? | Stable across renders? | Use when |
|---|---|---|---|
useId() | Yes | Yes | Component-scoped IDs that appear in rendered output. |
Math.random() | No (server and client diverge, causing hydration mismatch) | No (changes every render) | Never for render output. Only suitable for non-rendered scratch values. |
crypto.randomUUID() | No (same reason) | No | When a globally unique ID needs to be stored in your data, not generated during render. |
nanoid / uuid package | No (when called during render) | No (same reason) | For IDs persisted in your data model. Generate them outside render and store the result. |
| Module-level counter incremented during render | No (server and client may increment in different orders) | No (each render increments it again) | Do not use it for IDs in rendered output. |
Use useId when a component needs to generate an ID for rendered accessibility relationships. An ID supplied through props or stored in application data is also safe to render. If an ID belongs to your data model (a row's primary key, for example), generate it outside render and store it rather than replacing it with useId.
useId in React Server Components
useId behaves inside Client Components the same way it does in plain client React. Two caveats apply specifically to RSC setups:
- It works in synchronous Server Components but is not supported in async Server Components. If an ID is required inside an async Server Component, generate it outside render or move the rendering into a Client Component.
- IDs are derived from the component's position in the fiber tree, including any surrounding Suspense boundaries. There is no per-Suspense namespace. The boundary is simply another node in the parent path, which is what keeps IDs in different boundaries distinct.
- Server Actions are unrelated to ID generation. They run on the server and do not participate in render-time ID generation.
Uniqueness scope and multiple roots
IDs from useId are unique within a single React root. If your page mounts more than one root (for example, an island-style architecture or an embedded widget), separate roots can generate the same opaque ID. Do not depend on the exact generated string: React changed its default prefix in React 19.2.
To prevent that, give each root a distinct identifierPrefix:
import { createRoot } from 'react-dom/client';createRoot(document.getElementById('widget-a'), {identifierPrefix: 'a-',}).render(<WidgetA />);createRoot(document.getElementById('widget-b'), {identifierPrefix: 'b-',}).render(<WidgetB />);
The same option exists on hydrateRoot.
When to use useId
The most common use is associating a <label> with its form control for accessibility:
import { useId } from 'react';function NameField() {const id = useId();return (<div><label htmlFor={id}>Name:</label><input id={id} type="text" /></div>);}
It is also useful for ARIA attributes such as aria-describedby, aria-labelledby, and aria-controls.
Pairing inputs with aria-describedby and aria-labelledby
A common useId pattern for accessibility is connecting an input to a hint, an error message, or an external label via aria-describedby or aria-labelledby. Call useId once per component and append suffixes for each related element. This keeps related IDs grouped, saves hook calls, and makes the relationships clear in the markup:
function PasswordField() {const id = useId();return (<><label htmlFor={`${id}-password`}>Password:</label><inputid={`${id}-password`}type="password"aria-describedby={`${id}-hint`}/><p id={`${id}-hint`}>Must be at least 12 characters.</p></>);}
The same pattern applies to aria-labelledby (when the label is a separate element, not a <label>), aria-controls (a button that toggles a panel), and aria-errormessage (an input pointing at an error region). All of these attributes need stable IDs that survive SSR, which is what useId provides.
What useId is not for
The generated identifier represents a component position, not the identity of application data.
useId is not a list key
key identifies a piece of data across renders so React can reconcile additions, removals, and reorders. useId identifies a component position in the tree, which is the wrong identity for a list item. Always use a stable ID from the data, such as a database ID or slug.
- Do not use it as a list
key. UsinguseIdas a key produces output that renders correctly at first but breaks when the list is reordered or items are inserted. See the callout above for the reason. - Avoid it in CSS selectors and
document.querySelector. The generated format (e.g.:r0:) contains colons, which are valid in HTMLidattributes but require escaping in CSS selectors. Pair<label htmlFor>with<input id>instead of selecting by id. - Do not parse or rely on the format. The exact shape (
:r0:,«r0», etc.) is an implementation detail and has changed between versions. - Do not generate IDs for data with
useId. If the ID must outlive the component (saved to a database, sent in an API request, used as a stable record identifier),useIdis not the right tool. Usecrypto.randomUUID()or a UUID library and store the result.
Practical guidance
Use the opaque value only to connect rendered elements, and store data identities separately:
- Always pair the generated id with the element via
htmlFor/id(or the relevantaria-*attribute) — never use it as decoration. - Concatenate suffixes for related controls instead of calling
useIdrepeatedly. - Set
identifierPrefixon each root if your app mounts more than one. - Reach for
useIdonly when you actually need a generated id; many components can simply accept anidprop from the parent.
Further reading
- React documentation on
useId - MDN Web Docs on accessibility
- React 18 release notes
- React 19.2 release notes