useId

Generate a stable, unique id string for accessibility attributes that matches between server and client.

Signature
const id = useId()
  • Added in React 18.0.0

Return value

stringA unique ID string associated with this particular useId call in this particular component. The value is stable for the lifetime of the component instance and matches between the server-rendered HTML and the client during hydration.

Errors

Nothing, but calling it outside the top level of a component or Hook triggers React's invalid-hook-call error, and it cannot be called inside an async Server Component.

Changelog

  • PHP 18.0.0Introduced as a way to generate unique IDs on both the client and the server without causing hydration mismatches.
  • PHP 19.1.0The generated ID format changed from `:r123:` to `«r123»` so the value is a valid CSS selector.
  • PHP 19.2.0The generated ID format changed again to use an underscore instead of `:`. Treat the string as opaque rather than matching on its shape.

useId returns a string that is stable for one call site in one component instance and unique against every other call site in the tree. Its purpose is wiring accessibility attributes: htmlFor/id, aria-describedby, aria-labelledby.

When to reach for it

Reach for it whenever a reusable component needs a DOM id it cannot hardcode. A literal id="email" breaks the second time that component appears on a page: two elements share an id and clicking the label focuses the wrong input. A counter or Math.random() looks fine until you server-render, at which point the server and the client produce different strings, React reports a hydration mismatch, and the association is silently lost.

What trips people up

  • It is not a key. List keys must come from your data, or React cannot match rows across renders.
  • It is not a cache key for use(). The id is stable once mounted but may change during rendering.
  • One call covers many elements. Call it once per component and append suffixes such as id + '-firstName', rather than calling the Hook per field.
  • The format is opaque and has changed across React 18, 19.1 and 19.2. Never parse it or hand-build a selector from it.
  • Multiple React roots on one page can produce colliding ids; pass identifierPrefix to createRoot or hydrateRoot.

Example

Try it yourself
Loading editor…
Loading React…

When to use it

  • Give a design-system text field a label that focuses the right input even when the field is rendered several times on one page.
  • Point aria-describedby at a hint paragraph inside a reusable password component.
  • Generate matching ids for a whole fieldset from a single call by appending suffixes.
  • Render two independent React apps on one page without their generated ids colliding.

Frequently asked questions

Can I use useId as the key in a list?

No. Keys must be derived from your data, such as a record's id. `useId` is tied to a component instance, so React cannot use it to match items across renders, and the docs explicitly warn against it — the same warning applies to using it as a cache key for `use()`.

Why does the id look like «r0» or contain colons?

That is React's internal counter format, and it is deliberately opaque. React 18 emitted `:r0:`, React 19.1 changed it to `«r0»` so the value is a valid CSS selector, and React 19.2 switched to underscores. Never parse the string or build a selector by hand from its shape.

Do I need one useId call per input?

No, and it is wasteful. Call it once per component and append suffixes like `id + '-email'` and `id + '-password'`. One call already gives you a base string that is unique against every other component instance in the tree.

More examples

One call, several related ids

Three inputs, one Hook call. The output is a form whose labels resolve to `«r0»-street`, `«r0»-city` and `«r0»-postcode` (the exact prefix depends on the React version). Rendering `<AddressForm />` twice produces two disjoint sets, because each component instance gets its own base id.

Example · jsx
import { useId } from "react";

export default function AddressForm() {
  const id = useId();
  return (
    <form>
      <label htmlFor={id + "-street"}>Street</label>
      <input id={id + "-street"} type="text" />
      <hr />
      <label htmlFor={id + "-city"}>City</label>
      <input id={id + "-city"} type="text" />
      <hr />
      <label htmlFor={id + "-postcode"}>Postcode</label>
      <input id={id + "-postcode"} type="text" />
    </form>
  );
}

Wiring aria-labelledby and aria-describedby

A screen reader announces the group heading, the label and the hint for whichever field has focus. Because both `PasswordField` instances derive their ids from separate `useId` calls, the second field's hint never gets announced for the first field — the bug you get from a hardcoded `id="password-hint"`.

Example · jsx
import { useId } from "react";

function PasswordField() {
  const id = useId();
  const inputId = id + "-input";
  const hintId = id + "-hint";
  const groupId = id + "-group";

  return (
    <div role="group" aria-labelledby={groupId}>
      <h4 id={groupId}>Password</h4>
      <label htmlFor={inputId}>New password</label>
      <input id={inputId} type="password" aria-describedby={hintId} />
      <p id={hintId}>Must contain at least 18 characters.</p>
    </div>
  );
}

export default function ResetPasswordPage() {
  return (
    <>
      <PasswordField />
      <PasswordField />
    </>
  );
}

Two React roots on one page

`useId` only guarantees uniqueness within one root. On the client the counter is a module-level variable shared by every root in one copy of React, but ids generated during server rendering and hydration come from each root's own tree path, so without `identifierPrefix` two independent apps can emit identical strings and a label in the support widget can point at an input in the checkout widget. When you do pass a prefix, React embeds it inside the generated id rather than in front of it: under React 19.1 the ids read `«checkout-r0»` and `«support-r0»`, and the surrounding delimiters differ by React version — React 18 used colons and 19.2 uses underscores. This example uses a build setup and two DOM containers, so it is shown rather than run.

Example · jsx
// index.js — mounting two independent apps into the same document
import { createRoot } from "react-dom/client";
import Widget from "./Widget.js";

const checkout = createRoot(document.getElementById("checkout"), {
  identifierPrefix: "checkout-",
});
checkout.render(<Widget />);

const support = createRoot(document.getElementById("support"), {
  identifierPrefix: "support-",
});
support.render(<Widget />);

// If the HTML was server-rendered, the same prefix must be passed there:
//
//   renderToPipeableStream(<Widget />, { identifierPrefix: 'checkout-' })
//   hydrateRoot(container, <Widget />, { identifierPrefix: 'checkout-' })

Discussion

  • Be the first to comment on this lesson.