What is useId hook in React?
useId is a React 18 hook that generates a stable, unique ID guaranteed to match between server and client rendering.
Theory
TL;DR
useIdproduces IDs like:r0:,:r1:using React's internal counter, not randomness- Main difference from
Math.random(): server and client always produce the exact same value - Use it for
htmlFor/idpairs andaria-*attributes in SSR apps; skip it for pure client-side code - Never call it inside
.map()- call once per component, then append stable data
Quick example
import { useId } from 'react';
function EmailInput() {
const id = useId(); // Same ":r0:" on server and client
return (
<>
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
</>
);
}One call, one stable ID. Server renders :r0:, browser hydrates with :r0:. No mismatch warning.
Why random IDs break SSR
When React renders on the server, it outputs HTML. The browser then receives that HTML and React hydrates it - attaching event listeners and reconciling the virtual DOM. If any attribute value differs between server HTML and what React generates client-side, you get a hydration mismatch warning.
Math.random() gives "0.123..." on the server and "0.456..." on the client. Different values break the htmlFor/id link. Screen readers lose the label-to-input association. useId avoids this because React's counter runs the same sequence on both sides.
How it works
React keeps a global counter per component tree root. During SSR it increments that counter as it renders, then serializes the state. On the client, hydrateRoot starts from the same index and replays - so every useId call lands on the same number it did on the server.
The colon wrapping (:r1:) is intentional. Colons have special meaning in CSS (pseudo-classes, pseudo-elements), so an ID like :r0: cannot be accidentally targeted by a stylesheet. These IDs are for accessibility linking, not CSS.
When to use
htmlFor/idpairs in forms with SSRaria-labelledbyandaria-describedbyin accessible components (tooltips, modals, descriptions)- Multiple IDs in one component - call
useId()once, then append suffixes:${id}-email,${id}-password
Skip it for pure client-side apps. Any unique string works there. Skip it for data-testid too - just hardcode "email-input" in tests. Using useId for test attributes adds React allocator overhead with no benefit.
Common mistakes
Calling useId inside .map()
// ❌ New allocator slot on every render - IDs change
{users.map(u => <input key={u.id} id={useId()} />)}
// ✅ Call once per component, append stable data
function UserInput({ user }) {
const baseId = useId();
return <input id={`${baseId}-${user.id}`} />;
}Each useId() call inside a loop opens a new slot on every render. Server and client counters fall out of sync.
Using it for list keys
// ❌ Wrong
{items.map(item => <li key={useId()}>...</li>)}
// ✅ Use data from the item itself
{items.map(item => <li key={item.id}>...</li>)}Keys must be stable across renders. useId inside a map produces different IDs each time.
Assuming global uniqueness across multiple roots
IDs are scoped per root. Portals and separate ReactDOM.createRoot calls each have their own counter, so :r1: can appear in more than one tree. If they share the DOM, add a manual prefix: useId() + '-modal'.
Real-world usage
- Material-UI v6+: input
aria-describedbylinks generated internally withuseId - React Aria (Adobe): all form label associations use
useId - Chakra UI: modal
aria-labelledbyrelies on it - Next.js App Router: server components get stable form field IDs without extra config
Follow-up questions
Q: Why does the ID use colons like :r0:?
A: Colons avoid CSS conflicts. An ID starting with : cannot be selected with a plain CSS rule, which is intentional - these IDs exist for accessibility, not styling.
Q: What happens with multiple roots or portals?
A: Each root has its own counter. If two roots share the DOM, their IDs can overlap. Add a manual prefix to prevent collisions.
Q: Does React StrictMode break useId?
A: No. StrictMode double-invokes effects in development but preserves the ID counter for hydration. Production SSR is unaffected.
Q: Can I seed or customize the generated IDs?
A: No public API exists for that. If you need readable IDs in tests, use hardcoded data-testid values instead.
Q: (Senior) How does useId interact with Suspense boundaries in streaming SSR?
A: Each Suspense boundary tracks the ID counter independently. When a lazy subtree streams in, it picks up the correct sequence. Mismatches happen only if you violate hook call order inside a boundary.
Examples
Login form with label-input pairs
import { useId } from 'react';
function LoginForm() {
const id = useId(); // One base ID for the whole form
return (
<form>
<label htmlFor={`${id}-email`}>Email</label>
<input id={`${id}-email`} type="email" />
<label htmlFor={`${id}-password`}>Password</label>
<input id={`${id}-password`} type="password" />
</form>
);
}One useId call gives a base like :r3:. Both fields get stable IDs (:r3:-email, :r3:-password) without a second hook call. In my experience, this suffix pattern is cleaner than calling useId twice - fewer allocator slots, same result.
Accessible tooltip with aria-describedby
import { useId } from 'react';
function Tooltip({ text, children }: { text: string; children: React.ReactNode }) {
const id = useId();
return (
<div>
<div aria-describedby={id}>{children}</div>
<div role="tooltip" id={id}>{text}</div>
</div>
);
}aria-describedby on the trigger and id on the tooltip need to match for assistive technology to read the description. On a server-rendered page, useId guarantees that match without any manual coordination.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.