ReactuseId Hook

useId Hook

Accessible HTML requires that form inputs have unique IDs that match their labels via htmlFor. When a component renders multiple times on the same page, you need a way to generate unique IDs for each instance. React 18 introduced useId to solve this problem correctly — including across server-side rendering, where naive approaches like Math.random() or incrementing counters cause hydration mismatches.

The Problem: Why Not Math.random()?

When React renders on the server (SSR) and again on the client during hydration, both renders must produce identical output. Math.random() generates a different value on the server and on the client — React notices the mismatch and throws a hydration error.

JSX
// ❌ Math.random() — different on server vs client = hydration error
function Field({ label }) {
  const id = Math.random().toString(36).slice(2) // 'x2f4p' on server, 'k9a2m' on client

  return (
    <div>
      <label htmlFor={id}>{label}</label>    {/* server: htmlFor="x2f4p" */}
      <input id={id} />                       {/* client: id="k9a2m" — MISMATCH */}
    </div>
  )
}

// ❌ Module-level counter — resets between server and client
let counter = 0
function Field({ label }) {
  const id = `field-${counter++}` // may be 'field-0' on server, 'field-3' on client
  // (if other components called this before, the count differs)
  ...
}
The Solution: useId

JSX
import { useId } from 'react'

function Field({ label }) {
  // Stable, unique ID — same on server and client for this component instance
  const id = useId()

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  )
}

// Renders four times → four different, stable IDs:
// :r0: :r1: :r2: :r3:
<Field label="Name" />
<Field label="Email" />
<Field label="Password" />
<Field label="Bio" />

useId generates a unique string like :r0:, :r1:, etc. — the colon characters are intentional and valid in HTML id attributes (they make the IDs impossible to accidentally collide with CSS selectors, which use : as a pseudo-class delimiter).

A Reusable FormField Component

The real power of useId is building reusable form field components that you can drop anywhere on a page without ID collisions.

TSX
import { useId } from 'react'

type FormFieldProps = {
  label: string
  type?: string
  required?: boolean
  helpText?: string
  error?: string
}

function FormField({ label, type = 'text', required, helpText, error }: FormFieldProps) {
  const id = useId()
  const helpId = `${id}-help`
  const errorId = `${id}-error`

  return (
    <div style={{ marginBottom: 16 }}>
      <label htmlFor={id}>
        {label}
        {required && <span aria-hidden="true"> *</span>}
      </label>

      <input
        id={id}
        type={type}
        required={required}
        aria-describedby={[helpText && helpId, error && errorId]
          .filter(Boolean)
          .join(' ') || undefined}
        aria-invalid={error ? 'true' : undefined}
        style={{ display: 'block', width: '100%' }}
      />

      {helpText && (
        <p id={helpId} style={{ fontSize: '0.85em', color: '#666' }}>
          {helpText}
        </p>
      )}

      {error && (
        <p id={errorId} role="alert" style={{ color: 'red', fontSize: '0.85em' }}>
          {error}
        </p>
      )}
    </div>
  )
}

// Usage — each instance gets its own unique, accessible IDs
function RegistrationForm() {
  return (
    <form>
      <FormField
        label="Full name"
        required
        helpText="As it appears on your ID"
      />
      <FormField
        label="Email"
        type="email"
        required
        error="Please enter a valid email address"
      />
      <FormField
        label="Password"
        type="password"
        required
        helpText="At least 8 characters"
      />
      <button type="submit">Register</button>
    </form>
  )
}
Generating Multiple IDs from One useId Call

You only need to call useId once per component. To generate multiple related IDs, suffix the base ID string. This keeps the hook calls minimal and the relationship between IDs explicit.

JSX
import { useId } from 'react'

function DateRangePicker({ label }) {
  // One useId call, multiple derived IDs
  const id = useId()
  const startId = `${id}-start`
  const endId = `${id}-end`
  const legendId = `${id}-legend`

  return (
    <fieldset aria-labelledby={legendId}>
      <legend id={legendId}>{label}</legend>

      <div>
        <label htmlFor={startId}>Start date</label>
        <input id={startId} type="date" />
      </div>

      <div>
        <label htmlFor={endId}>End date</label>
        <input id={endId} type="date" />
      </div>
    </fieldset>
  )
}

// Both DateRangePickers get completely independent sets of IDs:
// First:  ":r0:-start", ":r0:-end", ":r0:-legend"
// Second: ":r1:-start", ":r1:-end", ":r1:-legend"
<DateRangePicker label="Booking dates" />
<DateRangePicker label="Availability window" />
Why Not Use the Array Index?

Using the index from a .map() as an ID is tempting but fragile:

Approach

Problem

Math.random()

Different on server vs client — hydration mismatch

Module counter (let id = 0)

Resets between server/client renders — mismatch

Array index

Changes when items are reordered — IDs shift, breaking label associations

useId()

Stable, unique, SSR-safe — correct in all cases

Note
`useId` generates IDs based on the component's position in the React tree — the same tree structure on server and client means the same IDs. This is why it is SSR-safe while approaches based on random numbers or global counters are not.
useId with aria- Attributes

useId is useful beyond htmlFor/id pairs. Any ARIA relationship that links elements by ID benefits from it:

JSX
import { useId } from 'react'

// Expandable accordion with proper ARIA
function Accordion({ title, children }) {
  const [open, setOpen] = useState(false)
  const id = useId()
  const headerId = `${id}-header`
  const panelId = `${id}-panel`

  return (
    <div>
      <button
        id={headerId}
        aria-expanded={open}
        aria-controls={panelId}
        onClick={() => setOpen(o => !o)}
      >
        {title}
      </button>

      <div
        id={panelId}
        role="region"
        aria-labelledby={headerId}
        hidden={!open}
      >
        {children}
      </div>
    </div>
  )
}

// Combobox / autocomplete
function Combobox({ label, options }) {
  const id = useId()
  const listboxId = `${id}-listbox`

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        role="combobox"
        aria-autocomplete="list"
        aria-controls={listboxId}
      />
      <ul id={listboxId} role="listbox">
        {options.map(opt => (
          <li key={opt.value} role="option">{opt.label}</li>
        ))}
      </ul>
    </div>
  )
}
What useId Does NOT Do
  • Not for list keys — use stable data IDs or index for key props, not useId()

  • Not a UUID generator — do not use it for database IDs or unique entity identifiers

  • Not for CSS selectors — the : characters in generated IDs break CSS selectors (this is intentional to prevent accidental styling)

Warning
Never use `useId` as a React list `key`. The key must be stable across renders but also unique within a list — `useId` generates one stable ID per component *call*, which is not related to list item identity.
Tip
A good rule: if you find yourself writing `id={`field-${Math.random()}`}` or `id={`field-${index}`}` in a reusable component, replace it with `useId()`. The result is always more correct, more accessible, and SSR-safe.