HP3004 (duplicate-use-id) means two or more React roots on the same page use
useId without an identifierPrefix, so each root generates the same ids.
Labels and ARIA references can then point at an element in the wrong root. Give
every root its own identifierPrefix, the same on the server and the client.
| Code | HP3004 |
|---|---|
| Name | duplicate-use-id |
| Default severity | Warning |
| Group | Markup the browser repaired |
| What it means | Several React roots on the page use useId without an identifierPrefix, so they generate the same ids and labels or ARIA references can point at the wrong element. |
What the HP3004 duplicate use id finding means
useId derives an id from the component's position in its root. Two roots with
the same shape start from the same position, so they produce the same ids:
:R1: in React 18, «R1» in React 19.0 and 19.1, _R_1_ in React 19.2 and
later.
hydration-proof reports this code when an id that looks generated by useId
appears more than once and at least two hydrating roots on the page have no
identifierPrefix. Any other repeated id is HP3003. The
finding lists the roots without a prefix:
2 elements (<input>) have the React-generated id "_R_0_". 2 React roots on this page use useId without an identifierPrefix, so each one generates the same ids.
Roots without identifierPrefix: #header, #app.The page still hydrates without a mismatch. The bug is what the duplicate ids
connect: a <label htmlFor> in one root can focus an input in the other, and
aria-describedby can read out the wrong text.
Likely causes
- A custom server that calls
hydrateRootseveral times, for example for a header widget and the main app. - Micro-frontends or embedded widgets that each hydrate their own root.
- Astro islands: each
<astro-island>is its own React root. Astro sets anidentifierPrefixfor its islands, so on Astro this finding means something else hydrates without one (see Astro).
The unstable id cause covers the related problem of a prefix that differs between the server and the client.
How to fix it
Give every React root on the page its own identifierPrefix, and pass the same
value to the server render and to hydrateRoot, so the ids still match during
hydration:
import { renderToString } from "react-dom/server";
const headerHtml = renderToString(<Header />, { identifierPrefix: "header-" });
const appHtml = renderToString(<App />, { identifierPrefix: "app-" });import { hydrateRoot } from "react-dom/client";
hydrateRoot(document.getElementById("header")!, <Header />, {
identifierPrefix: "header-",
});
hydrateRoot(document.getElementById("app")!, <App />, {
identifierPrefix: "app-",
});renderToPipeableStream and renderToReadableStream take the same
identifierPrefix option.
Or render the parts in one root, for example with portals, so useId generates
unique ids. Portals render on the client only, so this fits parts that do not
need server HTML.
Catch it with ESLint
no-unstable-id keeps ids on useId instead of
random values or counters. The collision between roots depends on how the page
is assembled, so only a test of the rendered page finds it.
When it is intentional
If the duplicate ids are never referenced (no labels, no ARIA attributes), you
can add an ignore.issues rule with code: "HP3004" and a reason; see
ignoring findings. A prefix is still the safer choice.