Hydration Proof

Search documentation

Find a page or section

Fix the React useId hydration error

An id is only stable if both sides compute it the same way.

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:

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 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, and several React roots that generate the same ids as 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

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:

server.tsx
import { renderToPipeableStream } from "react-dom/server";
 
const { pipe } = renderToPipeableStream(<App />, {
  identifierPrefix: "checkout-",
});
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.
  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 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 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 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:

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 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 reports module-level variables changed during render, the counters behind many id bugs.

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.

npx hydration-proof test --probe