useId: Stable IDs That Survive SSR Hydration

Generating IDs with Math.random() or a counter breaks SSR because the server and client produce different values, causing a hydration mismatch. useId solves this by generating an ID that is deterministic on both sides.

June 7, 20262 min read

Accessibility requires linking <label> elements to their corresponding <input> using matching id and htmlFor attributes:

TSX
<label htmlFor="email-input">Email</label> <input id="email-input" type="email" />

Hardcoding "email-input" works in a single component. Render the same component twice on the same page and both instances share the same ID — which is invalid HTML and breaks screen readers that rely on ID uniqueness to associate labels with inputs.

The instinctive fix is generating IDs dynamically:

TSX
// Approach 1: Math.random() const id = Math.random().toString(36).slice(2); // Approach 2: a module-level counter let counter = 0; const id = `input-${counter++}`;

Both break server-side rendering. The server generates one ID, the client generates a different one during hydration, and React throws a mismatch error — because the HTML it tries to reconcile does not match what it rendered on the server.

useId: The Correct Solution

useId generates an ID that is deterministic and stable — identical between the server render and the client hydration, consistent across re-renders, and unique per component instance:

TSX
function EmailField() { const id = useId(); return ( <> <label htmlFor={id}>Email</label> <input id={id} type="email" /> </> ); }

Render this component once — the ID might be r1. Render it again elsewhere on the same page — it gets r2. Each instance is unique, the label and input within each instance share the same value, and the server and client agree on what that value is.

Multiple IDs from One Call

When a single component needs to label multiple inputs, call useId once and derive suffixes:

TSX
function AddressForm() { const baseId = useId(); return ( <> <label htmlFor={`${baseId}-street`}>Street</label> <input id={`${baseId}-street`} type="text" /> <label htmlFor={`${baseId}-city`}>City</label> <input id={`${baseId}-city`} type="text" /> <label htmlFor={`${baseId}-zip`}>ZIP</label> <input id={`${baseId}-zip`} type="text" /> </> ); }

One call, three unique IDs, all guaranteed to be unique across the page and consistent between server and client.

Where useId Is Useful Beyond Labels

useId is not limited to form accessibility. Any element that needs a stable unique identifier across server and client benefits from it:

  • ARIA attributes that reference other elements by ID (aria-describedby, aria-controls, aria-labelledby)
  • Collapsible sections where a trigger button references the panel it controls
  • Tabs where tab buttons reference their panel content

The One Mistake to Avoid

Do not use useId to generate key props when mapping over a list:

TSX
// Wrong {items.map(item => ( <li key={useId()}>{item.name}</li> // also illegal — hooks in loops ))}

Beyond the hooks-in-loops violation, this is semantically wrong. key props need to come from your data — the item's actual ID, a stable index, or a unique field. React uses key to track which list items are which across renders. useId generates an ID per component instance, which is a completely different concept.

useId is for labelling DOM elements. key comes from your data.

Practice what you just read.

useId & SSR Quiz
1 exercise