React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

accessibilityidsssrhydrationhooks

useId Hook

Generate a unique, stable ID for accessibility attributes that is consistent between server and client.

Recipe

Quick-reference recipe card — copy-paste ready.

const id = useId();
 
// Use for accessible form elements
<label htmlFor={id}>Email</label>
<input id={id} type="email" />
 
// Derive multiple IDs from one
<label htmlFor={`${id}-first`}>First Name</label>
<input id={`${id}-first`} />
<label htmlFor={`${id}-last`}>Last Name</label>
<input id={`${id}-last`} />

When to reach for this: You need a unique ID for htmlFor, aria-labelledby, aria-describedby, or any DOM attribute that requires an ID — especially in SSR or reusable components.

Working Example

"use client";
 
import { useId, useState } from "react";
 
interface TextFieldProps {
  label: string;
  helpText?: string;
}
 
export function TextField({ label, helpText }: TextFieldProps) {
  const id = useId();
  const helpId = `${id}-help`;
  const [value, setValue] = useState("");
 
  return (
    <div className="space-y-1">
      <label htmlFor={id} className="block text-sm font-medium">
        {label}
      </label>
      <input
        id={id}
        value={value}
        onChange={(e) => setValue(e.target.value)}
        aria-describedby={helpText ? helpId : undefined}
        className="border rounded px-3 py-2 w-full"
      />
      {helpText && (
        <p id={helpId} className="text-xs text-gray-500">
          {helpText}
        </p>
      )}
    </div>
  );
}
 
export function SignupForm() {
  return (
    <form className="space-y-4 max-w-sm">
      <TextField label="Email" helpText="We'll never share your email." />
      <TextField label="Password" helpText="At least 8 characters." />
      <TextField label="Username" />
    </form>
  );
}

What this demonstrates:

  • Each TextField instance gets its own unique ID from useId
  • The ID links <label> to <input> via htmlFor and links help text via aria-describedby
  • Multiple instances of the same component coexist without ID collisions
  • IDs are stable across server and client renders, avoiding hydration mismatches

Deep Dive

How It Works

  • useId generates an ID based on the component's position in the React tree
  • The ID is deterministic — the same component in the same tree position always gets the same ID
  • The generated string includes a : character (e.g., :r1:) to avoid collisions with user-defined IDs
  • On the server, React generates the same IDs as on the client, ensuring hydration consistency
  • Multiple useId calls in the same component produce different IDs

Parameters & Return Values

ParameterTypeDescription
(none)useId takes no parameters
ReturnTypeDescription
idstringUnique ID string (e.g., :r1:, :r2:)

Variations

Accessible listbox:

function Listbox({ label, options }: ListboxProps) {
  const id = useId();
  const labelId = `${id}-label`;
  const listId = `${id}-list`;
 
  return (
    <div>
      <span id={labelId}>{label}</span>
      <ul id={listId} role="listbox" aria-labelledby={labelId}>
        {options.map((opt, i) => (
          <li key={opt.value} id={`${id}-option-${i}`} role="option">
            {opt.label}
          </li>
        ))}
      </ul>
    </div>
  );
}

Custom identifierPrefix for micro-frontends:

// In createRoot or hydrateRoot
const root = createRoot(container, {
  identifierPrefix: "app1-",
});
// Generated IDs: ":app1-r1:", ":app1-r2:", etc.

TypeScript Notes

// useId always returns a string — no generic needed
const id: string = useId();
 
// When building a component library, accept an optional override
interface InputProps {
  id?: string;
  label: string;
}
 
function Input({ id: propId, label }: InputProps) {
  const generatedId = useId();
  const inputId = propId ?? generatedId;
  return (
    <>
      <label htmlFor={inputId}>{label}</label>
      <input id={inputId} />
    </>
  );
}

Gotchas

  • Do not use for list keysuseId generates a single ID per hook call, not per list item. It is not suitable for key props. Fix: Use data-driven keys (item.id) or stable identifiers for list keys.

  • IDs contain colons — The generated ID format (:r1:) includes colons, which are valid in HTML id attributes but may cause issues with CSS selectors like #\:r1\:. Fix: Use [id="value"] attribute selectors or escape colons in CSS.

  • Cannot use conditionally — Like all hooks, useId cannot be called inside conditions or loops. Fix: Call useId at the top level and derive sub-IDs with string concatenation.

  • Multiple roots without prefix — Two separate React roots on the same page may generate colliding IDs. Fix: Use the identifierPrefix option in createRoot or hydrateRoot.

Alternatives

AlternativeUse WhenDon't Use When
crypto.randomUUID()Client-only app, no SSRSSR is involved — causes hydration mismatches
Counter-based IDOutside React (utility functions)Inside components — not deterministic across server/client
useRef with lazy initYou need a stable random value, not a tree-position-based IDYou need SSR-safe accessibility attributes
HTML <label> wrappingLabel wraps the input directly, no id neededInput and label are separated in the DOM

Why not just use a random ID? Random IDs generated during render differ between server and client, causing hydration mismatches and React warnings. useId solves this by deriving IDs from the component tree.

FAQs

Why can't I just use Math.random() or crypto.randomUUID() for element IDs?
  • Random IDs generated during render differ between server and client, causing hydration mismatches.
  • useId derives IDs from the component's position in the React tree, so server and client produce the same ID.
  • Use useId whenever SSR is involved.
Can I use useId to generate keys for list items?
  • No. useId generates one ID per hook call, not per list item.
  • It is designed for accessibility attributes (htmlFor, aria-labelledby), not for key props.
  • Use data-driven keys (item.id) or stable identifiers for lists.
How do I derive multiple IDs from a single useId call?
const id = useId();
const nameId = `${id}-name`;
const emailId = `${id}-email`;
const helpId = `${id}-help`;
  • Use string concatenation to create sub-IDs from one useId call.
Gotcha: Why do generated IDs contain colons, and can that cause CSS issues?
  • useId generates IDs like :r1: with colons to avoid collisions with user-defined IDs.
  • Colons are valid in HTML id attributes but must be escaped in CSS selectors: #\:r1\:.
  • Prefer attribute selectors [id="value"] instead of ID selectors in CSS.
What happens if two separate React roots on the same page generate conflicting IDs?
  • Without configuration, two roots may produce the same IDs (:r1:, :r2:, etc.).
  • Use the identifierPrefix option in createRoot or hydrateRoot to namespace IDs.
const root = createRoot(container, { identifierPrefix: "app1-" });
// IDs: ":app1-r1:", ":app1-r2:", etc.
Can I call useId conditionally or inside a loop?
  • No. Like all hooks, useId must be called at the top level of your component.
  • Call it once and derive sub-IDs with string concatenation for conditional or repeated elements.
How do I type useId in TypeScript?
// useId always returns string -- no generic needed
const id: string = useId();
  • There is no generic parameter. The return type is always string.
How do I build a reusable component that accepts an optional id prop but falls back to useId?
interface InputProps {
  id?: string;
  label: string;
}
 
function Input({ id: propId, label }: InputProps) {
  const generatedId = useId();
  const inputId = propId ?? generatedId;
  return (
    <>
      <label htmlFor={inputId}>{label}</label>
      <input id={inputId} />
    </>
  );
}
Gotcha: Is the generated ID format stable across React versions?
  • The exact format (e.g., :r1:) is an implementation detail and may change between React versions.
  • Do not parse or depend on the internal structure of the generated ID.
  • Treat it as an opaque string.
Does calling useId multiple times in the same component produce different IDs?
  • Yes. Each useId() call in the same component returns a unique ID.
  • This is useful when a component needs multiple unrelated ID attributes.
  • All generated IDs remain stable across server and client renders.
  • useRef — access DOM elements directly without IDs
  • Custom Hooks — build accessible form primitives using useId
  • use — React 19 primitives that work alongside useId