# HP3004: Two React roots generate the same ids

> HP3004 (duplicate-use-id) means several React roots on one page call useId without an identifierPrefix and generate the same ids. Give each root a prefix.

Source: https://hydration.jscrate.dev/docs/issues/hp3004
Last updated: 2026-09-18

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](https://hydration.jscrate.dev/docs/issues/hp3003). The
finding lists the roots without a prefix:

```text
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 `hydrateRoot` several 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 an
  `identifierPrefix` for its islands, so on Astro this finding means something
  else hydrates without one (see [Astro](https://hydration.jscrate.dev/docs/frameworks/astro)).

The [unstable id](https://hydration.jscrate.dev/docs/causes/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:

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

const headerHtml = renderToString(<Header />, { identifierPrefix: "header-" });
const appHtml = renderToString(<App />, { identifierPrefix: "app-" });
```

```tsx title="entry-client.tsx"
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`](https://hydration.jscrate.dev/docs/rules/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](https://hydration.jscrate.dev/docs/ignoring). A prefix is still the safer choice.

## Related

- [HP3003: duplicate id attribute](https://hydration.jscrate.dev/docs/issues/hp3003)
- [Generated id differs between server and client](https://hydration.jscrate.dev/docs/causes/unstable-id)
- [Astro islands and hydration-proof](https://hydration.jscrate.dev/docs/frameworks/astro)
- [Adapters and custom servers](https://hydration.jscrate.dev/docs/adapters)
- [HP3001: invalid HTML nesting](https://hydration.jscrate.dev/docs/issues/hp3001)
