useId
useId produces a unique, stable string that you can attach to HTML attributes—most often id, htmlFor, and the family of aria-* references—so that labels, form fields, and descriptions can be wired together for assistive technology. Its defining feature is that the value it returns is identical on the server and on the client, which keeps server-rendered markup from breaking during hydration. It exists precisely because the obvious alternatives—hardcoded strings, array indexes, or Math.random()—all fall apart once a component is reused or rendered on both sides of an SSR boundary.
The hydration problem it solves
Accessible markup frequently needs to connect two elements by ID. A <label> points to its input with htmlFor, and aria-describedby points to a hint element. The IDs themselves are arbitrary, but they must be unique on the page and they must match between the two elements. The naive fix is to write a literal like id="email". That works once, but the moment the component is rendered twice the IDs collide, silently mis-associating labels with the wrong fields.
Generating the ID at runtime seems like the answer, until server-side rendering enters the picture. If the server renders id="r-0.482" and the browser later renders id="r-0.917", the attributes no longer match the server HTML and React reports a hydration mismatch. useId avoids both traps: each call yields a unique value, and React guarantees the same value is generated on server and client for the same position in the tree.
import { useId } from "react";
function EmailField() {
const id = useId();
return (
<div>
<label htmlFor={id}>Email address</label>
<input id={id} type="email" name="email" />
</div>
);
}
Render EmailField ten times and you get ten distinct, collision-free IDs—no manual bookkeeping, no mismatch warnings.
Do not use
useIdto generate keys for a list. Keys should come from your data, anduseIdreturns one stable value per call site, not one per item. Use it for HTML/ARIA wiring only.
Sharing one ID across several attributes
Often a single field needs more than one related ID—say, an input plus a hint and an error message it describes. Rather than calling useId multiple times, call it once and append suffixes. This keeps the IDs grouped, readable, and still unique per component instance.
import { useId } from "react";
function PasswordField() {
const id = useId();
return (
<div>
<label htmlFor={`${id}-input`}>Password</label>
<input
id={`${id}-input`}
type="password"
aria-describedby={`${id}-hint`}
/>
<p id={`${id}-hint`}>Use at least 12 characters.</p>
</div>
);
}
Each instance gets its own base («r0», «r1», …), and the -input and -hint suffixes derive deterministically from it, so the aria-describedby always resolves to the right hint.
Why not random values or indexes
It helps to see exactly why the common shortcuts fail, since each looks reasonable in isolation.
| Approach | Unique per instance | SSR-safe | Verdict |
|---|---|---|---|
Hardcoded string "email" | No | Yes | Collides when reused |
| Array index | Sometimes | Yes | Breaks on reorder, can collide across components |
Math.random() / Date.now() | Yes | No | Hydration mismatch; changes every render |
crypto.randomUUID() | Yes | No | Same SSR mismatch as random |
useId() | Yes | Yes | Correct on both server and client |
The randomness-based options are the most tempting and the most broken: a value computed during render changes between server and client (and even between renders), which is exactly what triggers React’s hydration warnings.
Configuring a global prefix
When a single page hosts multiple independent React roots—micro-frontends, widgets, or an island architecture—IDs from different roots could theoretically collide. Pass a unique identifierPrefix to each root so every generated ID is namespaced.
import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("checkout"), <Checkout />, {
identifierPrefix: "checkout-",
});
The same identifierPrefix option exists on the server renderer (for example renderToPipeableStream), and the values on both sides must match so hydration stays consistent.
What the returned string looks like
The exact format is an implementation detail and you should never parse or depend on it. In React 18 and 19 the value contains colon delimiters, which makes it a valid HTML id but not a valid CSS selector or querySelector argument without escaping.
Output:
:r0:
:r1:
checkout-:r0:
Because the value can contain
:, never feed a rawuseIdstring intodocument.querySelector('#' + id). Use it for theidattribute andhtmlFor/aria-*references, where the colon is perfectly legal.
Best Practices
- Reach for
useIdwhenever you need anidto wire a label, hint, or ARIA reference to a control inside a reusable component. - Call it once per component and append suffixes for multiple related IDs rather than calling the hook repeatedly.
- Never use it for list keys—derive keys from stable data instead.
- Don’t generate IDs with
Math.random(),Date.now(), orcrypto.randomUUID()in rendered output; they cause SSR hydration mismatches. - Treat the returned string as opaque; don’t parse it or use it directly in CSS selectors without escaping the colons.
- Set a matching
identifierPrefixon every React root when multiple roots share one page.