# Fix the React useId hydration error

> A React useId hydration error means an id differs between server and client: a counter, a random value, or useId in trees that differ. How to fix each.

Source: https://hydration.jscrate.dev/docs/causes/unstable-id
Last updated: 2026-09-18

A React useId hydration error means an element's id differs between the server
HTML and the first client render. The id came from a counter, a random value or
the clock, or `useId` ran in trees that differ. Use `useId` for every generated
id, render the same tree on both sides, and give each React root its own
`identifierPrefix`.

## Symptoms

React 18 says the prop `id` did not match, and prints the server and client
values:

```text
Warning: Prop `id` did not match. Server: "react-select-2-live-region" Client: "react-select-3-live-region"
Warning: Prop `htmlFor` did not match. Server: "field-5731" Client: "field-1"
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up.
```

React 19 reports attribute differences in development only. In production it
keeps the server's ids without a warning, so a `<label>` can point at nothing
and ARIA references (`aria-controls`, `aria-describedby`) break for screen
readers.

hydration-proof reports a differing id as [HP1002](https://hydration.jscrate.dev/docs/issues/hp1002) with
the cause **Generated id differs**, and recognizes React's own ids (`:r1:`,
`«r1»` or `_r_1_`, depending on the React version). It also reports ids used
twice on a page as [HP3003](https://hydration.jscrate.dev/docs/issues/hp3003), and several React roots that
generate the same ids as [HP3004](https://hydration.jscrate.dev/docs/issues/hp3004).

## Why a React useId hydration error happens

An id has to be computed the same way on the server and in the browser. These
do not qualify:

- **A module-level counter.** The server process keeps counting across every
  request (`field-5731`), while each browser tab starts at `field-1`.
- **Random values and the clock.** `Math.random()`, `crypto.randomUUID()`,
  `nanoid()`, lodash's `uniqueId` and `Date.now()` give a new value per render.
- **`useId` in trees that differ.** `useId` derives the id from the
  component's position in the tree, so it only matches when the server and the
  client render exactly the same tree above it. A component that renders
  something extra on one side (a `typeof window` branch, a client-only banner)
  shifts every id after it.
- **Several roots without a prefix.** Two React roots on one page both start
  their `useId` sequence from the same point, so they generate the same ids.

## How to fix it

### Use useId instead of counters and random ids

```tsx title="email-field.tsx"
import { useId } from "react";

// Before:
// let nextId = 0;
// const id = `email-${nextId++}`;

export function EmailField() {
  const id = useId();
  return (
    <>
      <label htmlFor={id}>Email</label>
      <input id={id} type="email" />
    </>
  );
}
```

For list items, combine `useId()` with a stable key from your data:
`` `${id}-${item.id}` ``. Never use `useId` for React `key` props.

### Give each React root its own identifierPrefix

When one page hydrates several independent React apps, give each root a
prefix, and use the same prefix on the server and the client:

```tsx title="server.tsx"
import { renderToPipeableStream } from "react-dom/server";

const { pipe } = renderToPipeableStream(<App />, {
  identifierPrefix: "checkout-",
});
```

```tsx title="client.tsx"
import { hydrateRoot } from "react-dom/client";

hydrateRoot(document.getElementById("checkout")!, <App />, {
  identifierPrefix: "checkout-",
});
```

With one app per page, no prefix is needed.

### useId hydration mismatch (App Router)

In the Next.js App Router, check what renders above the component whose id
differs:

1. Look for a Client Component above it that renders a different number of
   components on each side (`typeof window`, `useMediaQuery` without a server
   default, a mounted flag that adds siblings). Fix that component first; see
   [browser APIs in render](https://hydration.jscrate.dev/docs/causes/browser-api).
2. `useId` cannot be used in async Server Components. Call it in a Client
   Component or a synchronous component.
3. Keep React and Next.js on matching, current versions. Framework bugs have
   shifted ids before: [vercel/next.js#84029](https://github.com/vercel/next.js/issues/84029)
   reported different `useId` values on each side in some Next.js 15 releases,
   and was closed with a fix.

### Fix a Radix UI hydration error

Radix Primitives generate their ids on the server with React's `useId` on
React 18 and later (on older React versions they added ids only after
hydration). A Radix id hydration error today (`aria-controls` or `id` values
like `radix-_R_166itmdl5rlb_` on the server and `radix-_R_66itmdl5rlb_` in the
browser) nearly always means the component trees differ, as in the list above.
Version combinations matter too:
[radix-ui/primitives#3700](https://github.com/radix-ui/primitives/issues/3700)
reported this error with Next.js 15.5 and React 19.2, and downgrading Next.js
avoided it. Upgrade Radix, React and Next.js together. The
[shadcn/ui guide](https://hydration.jscrate.dev/docs/guides/shadcn-hydration-error) covers the components
built on Radix.

### Fix the react-select hydration error

react-select builds its ids from a module-level counter
(`react-select-2-live-region`) unless you pass `instanceId`. The server's
counter keeps growing across requests, so every react-select Next.js hydration
error has the same fix:

```tsx title="country-select.tsx"
"use client";

import { useId } from "react";
import Select from "react-select";

export function CountrySelect({
  options,
}: {
  options: { value: string; label: string }[];
}) {
  const instanceId = useId();
  return <Select instanceId={instanceId} options={options} />;
}
```

Other libraries with an id or prefix prop take the same approach: pass a value
from `useId`.

## Catch it with ESLint

[`no-unstable-id`](https://hydration.jscrate.dev/docs/rules/no-unstable-id) reports random values, the
clock and module-level counters that end up in an `id`, `htmlFor`, an `aria-*`
reference or a prop ending in `Id`, and points you to `useId`.
[`no-global-render-counter`](https://hydration.jscrate.dev/docs/rules/no-global-render-counter) reports
module-level variables changed during render, the counters behind many id
bugs.

```bash
npm install -D eslint-plugin-hydration-proof
```

## Catch it in CI

`hydration-proof test` reports differing ids as HP1002 even in production
builds, where React says nothing, and checks the whole document for duplicate
ids (HP3003) and roots that share `useId` ids (HP3004). With `--probe`, a
random id changes when only the random seed changes, which proves the cause. An
id from a server-side counter changes on every request, so an identical reload
already renders it differently. See [probes](https://hydration.jscrate.dev/docs/probes).

```bash
npx hydration-proof test --probe
```

## Related

- [Random values in render](https://hydration.jscrate.dev/docs/causes/random)
- [HP3004: two React roots generate the same ids](https://hydration.jscrate.dev/docs/issues/hp3004)
- [The no-unstable-id rule](https://hydration.jscrate.dev/docs/rules/no-unstable-id)
- [Attributes that didn't match](https://hydration.jscrate.dev/docs/errors/tree-hydrated-but-attributes-didnt-match)
- [shadcn/ui hydration errors](https://hydration.jscrate.dev/docs/guides/shadcn-hydration-error)
