A React useId hydration error means an id differs between the server HTML and
the first browser render. Check for a counter or random value first. If you
already use useId, check that both sides render the same tree and that
separate React roots use separate prefixes.
Symptoms
React 18 may print both id 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.An id mismatch can break a label or ARIA reference. React 19 may keep the server's id without reporting the difference in production.
hydration-proof reports a changed id as HP1002, a duplicate id as HP3003, and collisions between React roots as HP3004.
Why a React useId hydration error happens
An id must be stable across both renders. These patterns can break it:
- A module-level counter. The server process keeps counting across every
request (
field-5731), while each browser tab starts atfield-1. - Random values and the clock.
Math.random(),crypto.randomUUID(),nanoid(), lodash'suniqueIdandDate.now()give a new value per render. useIdin trees that differ.useIdderives 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 (atypeof windowbranch, a client-only banner) shifts every id after it.- Several roots without a prefix. Two React roots on one page both start
their
useIdsequence from the same point, so they generate the same ids.
How to fix it
Use useId instead of counters and random ids
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, use stable ids from the data. Do not use useId for React
key props.
Give each React root its own identifierPrefix
If a page has several React roots, give each a distinct prefix. Keep each root's prefix the same on the server and in the browser:
import { renderToPipeableStream } from "react-dom/server";
const { pipe } = renderToPipeableStream(<App />, {
identifierPrefix: "checkout-",
});import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("checkout")!, <App />, {
identifierPrefix: "checkout-",
});One root does not usually need a custom prefix.
useId hydration mismatch (App Router)
In the Next.js App Router, inspect the component tree above the changed id:
- Look for a Client Component above it that renders a different number of
components on each side (
typeof window,useMediaQuerywithout a server default, a mounted flag that adds siblings). Fix that component first; see browser APIs in render. useIdcannot be used in async Server Components. Call it in a Client Component or a synchronous component.- Keep React and Next.js on matching, current versions. Framework bugs have
shifted ids before: vercel/next.js#84029
reported different
useIdvalues on each side in some Next.js 15 releases, and was closed with a fix.
Fix a Radix UI hydration error
Radix Primitives use React's useId for generated ids. A Radix id hydration
error often points to a different component tree. Version combinations can
matter too:
radix-ui/primitives#3700
reported an id mismatch with Next.js 15.5 and React 19.2. Check compatible
updates for Radix, React, and Next.js. The
shadcn/ui guide covers the components
built on Radix.
Fix the react-select hydration error
A react-select Next.js hydration error can come from its instance counter.
If the server and browser counters differ, pass a stable instanceId:
"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} />;
}Use the same approach for other libraries that accept an id or prefix prop.
Catch it with ESLint
no-unstable-id flags ids built from random
values, time, or counters. no-global-render-counter
flags module-level counters changed during render.
npm install -D eslint-plugin-hydration-proofCatch it in CI
hydration-proof test checks ids in production too. Use --probe to see
whether a random seed changes the id. A server-side counter may change even
on an identical reload. See probes.
npx hydration-proof test --probe