skip to content

What problem does React's useId hook solve, and why are a module-level counter or Math.random() bad substitutes for it?

level: juniorimportance: should knowfreq 50%

answer

  1. ids must survive hydration
  2. server and client render separately
  3. randomness differs between the two passes
  4. derived from position in the tree
  5. label and aria wiring is the use case

basics

~20 s

useId generates a unique string id that is identical on the server and on the client, so attributes like htmlFor and aria-describedby survive hydration. A random value differs between the two renders, and a module counter depends on render order, so both can produce mismatched markup.

solid answer

~50 s

`useId` returns a unique string that React derives from the component's position in the tree, which makes it stable across server rendering and client hydration. That is the whole point: you need an id to wire a `<label htmlFor>` to an input, or an `aria-describedby` to its description, and if the server writes one id into the HTML while the client's first render computes a different one, hydration sees mismatched attributes and the accessibility wiring breaks. `Math.random()` or `crypto.randomUUID()` produce a different value on each render pass by construction, so they mismatch every time. A module-level counter is worse than it looks: it depends on how many components happened to render before this one, which differs between a server request and a fresh client hydration, and it does not reset between requests on a long-lived server. `useId` is also correct across multiple instances of the same component, since each instance occupies a different tree position.

code

javascript · 11 lines
javascript
import { useId } from 'react';

export function PasswordField() {
  const hintId = useId();
  return (
    <div>
      <input type="password" aria-describedby={hintId} />
      <p id={hintId}>Use at least 12 characters.</p>
    </div>
  );
}

go deeper

for a junior

Be ready to say useId gives you a unique id string that is the same on the server and the client, and that you use it to connect a label or an aria attribute to an input.

for a middle

Explain the hydration constraint concretely: the HTML and the first client render must agree, so React derives the id from tree position instead of generating randomness.

for a senior

Show why the common workarounds fail on their own terms — random values differ per pass, module counters depend on render order and leak across server requests — and treat the id as an opaque attribute value.

for a principal

Set the team convention: ids for accessibility wiring come from this hook, data identity comes from the domain, and multi-root pages namespace themselves with identifierPrefix rather than inventing per-app schemes.

## The requirement Accessible markup depends on ids that connect elements: ```jsx <label htmlFor={id}>Email</label> <input id={id} aria-describedby={hintId} /> <p id={hintId}>We never share it.</p> ``` The ids must be unique on the page — two inputs with the same id break both the label association and any assistive technology that follows it — and, in a server-rendered app, they must be *the same string* in the HTML the server produced and in the first render the client performs while hydrating. Those two requirements together are what makes id generation harder than it looks. ## Why hydration is the hard constraint A server-rendered page is produced twice: once as HTML on the server, and once as a client render that attaches React to that HTML. Hydration assumes the two agree. If the server wrote `for="a1"` and the client's first render computes `for="b7"`, the attribute the browser sees no longer matches what React thinks it rendered, and the association is wrong even when the page looks fine. `useId` solves this by not generating randomness at all. React derives the id from where the component sits in the tree, and since the server and the client render the same tree, they derive the same string. Call the hook in a component and every instance gets its own value, because each instance has its own position. ```jsx import { useId } from 'react'; function EmailField() { const id = useId(); return ( <> <label htmlFor={id}>Email</label> <input id={id} type="email" /> </> ); } ``` Render `<EmailField />` three times and you get three distinct, hydration-stable ids with no bookkeeping. ## Why the usual substitutes fail **`Math.random()` / `crypto.randomUUID()` / `Date.now()`.** These are non-deterministic by design. The server produces one value, the client's hydration render produces another, and the markup mismatches on every single render. Some codebases hide this by generating the id in a `useState` initializer or a ref — that stabilises it *within* a client session but still differs from the server's value, so the hydration problem is untouched. **A module-level counter** (`let n = 0; const id = 'field-' + n++`). This looks deterministic and is not, for two reasons. It depends on how many other components incremented it first, which is a function of render order and can differ between the server pass and the client pass. And on a long-lived server the module is shared across requests, so the counter keeps climbing and two users get different ids for the same component — including ids that no client render will ever reproduce. **A hard-coded string** (`id="email"`). Deterministic, hydration-safe, and broken as soon as the component is rendered twice on one page: duplicate ids on a document are invalid and the label associates with whichever element the browser resolves first. **A prop passed down from the parent** is a legitimate option — the parent chooses the id and passes it in — but it pushes uniqueness bookkeeping onto every call site, which is exactly the chore `useId` removes. ## Properties of the returned value - It is a **string**, suitable for use directly in `id`, `htmlFor`, `aria-describedby`, `aria-labelledby` and similar attributes. - It is **stable across renders** of the same component instance, so it does not change when state updates. - Its **format is opaque**. React does not guarantee the shape of the string, and it has changed between React versions, so never parse it, never assume it is a valid bare CSS selector, and never construct one by string-concatenating it into a stylesheet. Use it as an attribute value. - It is **not a data identity**. It identifies a rendered position, not a record, so it is the wrong thing to persist, send to a server, or use as a React `key`. ## Multiple roots on one page When two independent React roots render into the same document, their generated ids could in principle collide. React exposes an `identifierPrefix` option on the root creation APIs (`createRoot` and `hydrateRoot`, and the server rendering APIs) so each root can namespace its ids. This is a real answer to a real problem, but it is worth mentioning only when the interviewer raises multi-root or micro-frontend setups — it is not part of everyday usage. ## How to answer Lead with the hydration requirement, since that is why the hook exists rather than a utility function. Then name the accessibility use case it serves, and dismiss random and counter-based approaches with the specific reason each fails: non-determinism for one, render-order and cross-request dependence for the other.

  • Why is generating the id once in a useState initializer not good enough for a server-rendered app?
    It stabilises the id across re-renders on the client, which is the easy half. The server still produced a different value when it rendered the HTML, so the first client render disagrees with the markup it is hydrating. Determinism across renders is not the same as agreement between server and client.
  • Can you use the value returned by useId as the key for an item in a list?
    No. Keys must identify data so React can match items across reorders and updates, and useId identifies a rendered position instead — it stays the same when the data at that position changes. Keys come from stable properties of your items, such as a record id.
  • Two independent React roots render on the same page. How do you keep their generated ids from colliding?
    Give each root a distinct `identifierPrefix` when you create it. `createRoot` and `hydrateRoot` accept it as an option, and the server rendering APIs accept it too, so each root's ids live in their own namespace. It mainly matters for micro-frontends and embedded widgets.

saying these in an interview costs you the question

  • Says Math.random() is fine because it runs once
  • Uses useId values as list keys
  • Hard-codes one id and reuses the component twice
  • Thinks useId identifies data rather than tree position
  • Parses or slices the returned id string

context