10LOC
Reactintermediate

useId for stable, SSR-safe unique ids

Published March 30, 2027

import { useId } from "react";

export const PasswordField = () => {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>Password</label>
      <input id={id} type="password" aria-describedby={`${id}-hint`} />
      <p id={`${id}-hint`}>Must be at least 12 characters.</p>
    </div>
  );
};

What

useId generates a unique ID string that's stable across a component's lifetime and, critically, identical between the server-rendered HTML and the client's first render.

Why it matters

Associating a label with an input, or a hint with aria-describedby, needs an id — and if that id is hardcoded, rendering the same component twice on one page creates a duplicate, breaking the association for both. The instinctive fix, a module-level counter or Math.random(), breaks under server rendering: the server and the client each generate their own sequence, so the id baked into the server's HTML doesn't match the one the client computes on hydration, and React logs a mismatch warning (or worse, the association silently fails). useId is deterministic per component position in the tree, so server and client agree without coordination.

How it works

  • useId() takes no arguments and returns one ID per call, scoped to this component instance.
  • Calling it once and deriving related ids from it (`${id}-hint`) is the recommended pattern when a component needs several linked ids, rather than calling useId() multiple times for what's conceptually one identifier namespace.
  • htmlFor/id links the <label> to the <input>; aria-describedby/id links the hint text — both rely on the ids matching exactly, which useId guarantees without either side hardcoding a string.

Gotchas

  • Don't use useId for list keys — it produces one id per call, not per array item, and reusing it across a .map() produces duplicate keys.
  • The generated string (something like «r1») is intentionally not a clean, short token — don't parse it or rely on its format, only its stability and uniqueness.
  • If multiple independent React roots render on the same page, pass a distinct identifierPrefix to each createRoot/hydrateRoot call so their generated ids can't collide.

Related

  • crypto.randomUUID() — fine for ids that never touch SSR/hydration (e.g., a key for a purely client-side list built after mount), but wrong for anything that needs to match between server and client render.
  • aria-labelledby — an alternative to aria-describedby/htmlFor worth knowing for cases where the "label" is more complex markup than plain text.