useId for Stable, Unique IDs

Generate SSR-safe unique IDs for accessibility wiring — and never use it for keys.

useId gives you a unique, stable string ID that matches between the server-rendered HTML and the client hydration. Its job is accessibility plumbing: connecting a label to an input, an input to its error message via aria-describedby, a trigger to its menu via aria-controls.

Why not just hardcode an id?

If a component is rendered more than once on a page, hardcoded IDs collide — and duplicate IDs break htmlFor and ARIA associations. useId produces a different value per instance while staying deterministic across server/client, so hydration does not mismatch.

function Field({ label }) {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </>
  );
}

One id, many related elements

Need several related IDs? Call useId once and append suffixes: `${id}-input`, `${id}-error`. That keeps them grouped and still unique.

The rule you must not break

useId is not for list keys. Keys must come from your data's identity so React can track items across reorders; a generated id has no relationship to the data and defeats reconciliation. Use useId only for DOM/ARIA attributes.

Example

Example · javascript
import { useId, useState } from 'react';

function PasswordField() {
  const id = useId();
  const inputId = `${id}-input`;
  const hintId = `${id}-hint`;
  const [value, setValue] = useState('');
  const tooShort = value.length > 0 && value.length < 8;

  return (
    <div>
      <label htmlFor={inputId}>Password</label>
      <input
        id={inputId}
        type="password"
        value={value}
        onChange={(e) => setValue(e.target.value)}
        aria-describedby={hintId}
        aria-invalid={tooShort}
      />
      <p id={hintId}>
        {tooShort ? 'At least 8 characters.' : 'Use 8+ characters.'}
      </p>
    </div>
  );
}

// Rendering <PasswordField /> twice yields two non-colliding id sets.

When to use it

  • A reusable FormField component generates a unique ID with useId and links its label and error message to the input so multiple instances on the same page never share an ID.
  • A server-side rendered app uses useId to produce stable IDs that match between the server HTML and client hydration, avoiding hydration mismatches from random Math.random IDs.
  • A design system widget generates aria-controls, aria-labelledby, and aria-describedby attribute values from one useId base to wire up complex ARIA relationships correctly.

More examples

Unique label/input pairing

Generates a unique ID with useId to wire each label to its input, safe across multiple instances on the same page.

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

function FormField({ label, type = 'text' }) {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Multiple instances each get a unique id — no conflicts
// <FormField label="Name" />
// <FormField label="Email" type="email" />

Derive multiple IDs from one base

Derives multiple unique IDs from one useId call to wire label, hint, and error to the input via ARIA attributes.

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

function PasswordField({ label }) {
  const baseId = useId();
  const inputId = `${baseId}-input`;
  const hintId = `${baseId}-hint`;
  const errorId = `${baseId}-error`;

  return (
    <div>
      <label htmlFor={inputId}>{label}</label>
      <input
        id={inputId}
        type="password"
        aria-describedby={`${hintId} ${errorId}`}
      />
      <p id={hintId}>At least 8 characters.</p>
      <p id={errorId} role="alert" />
    </div>
  );
}

useId — do not use as list key

Shows the common misuse of useId as a list key — which also breaks rules of hooks — versus the correct stable-ID key.

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

// BAD — useId is for accessibility, not list keys
function WrongUsage({ items }) {
  return items.map(item => {
    const id = useId(); // breaks rules of hooks in a loop!
    return <li key={id}>{item.name}</li>;
  });
}

// GOOD — use stable data IDs for keys
function CorrectUsage({ items }) {
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Discussion

  • Be the first to comment on this lesson.