vercel / vercel/next.js

`useId()` in Server Components returns duplicate ids after a client-side navigation

Open
#98,337 0 comments 0 reactions 1 assignee View on GitHub

@icyJoseph is already working on this.

Since Sep 8, 2026.

Linking and Navigating React
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/YuichiroFukushi/next-rsc-useid-collision

To Reproduce
  1. npm install && npm run dev
  2. Open http://localhost:3000/other (a hard load).
  3. Click Back to / (a next/link client-side navigation).
  4. The home page's "dots" texture is drawn with the root layout's "lines" pattern, and both captions print the same id, _S_1_. Reload the page: the dots come back and the ids are _S_1_ and _S_2_.

The same happens with next build && next start. The repro's layout sets dynamic = "force-dynamic" so the routes render per request, as any route that reads cookies() or headers() does. A fully static route does not reproduce it, because a prerender renders the whole tree in one pass.

Reproduced on next@16.3.4 and next@16.4.0-canary.20 (the repro pins the canary). This is not a regression to bisect: the Flight renderer has never been given an identifierPrefix by Next.js, so there is no earlier version where the ids were unique.

Current vs. Expected behavior

Current. useId() in a Server Component returns ids from a counter that restarts at 1 for every RSC request. A client-side navigation renders only the changed segments (shared parent layouts are skipped), so the first useId() in the new segment returns _S_1_ again, which is the id a Server Component in the persisted root layout already put in the DOM. Anything resolved document-wide by id then points at the wrong element: fill="url(#…)", mask, clip-path, aria-labelledby, aria-describedby, htmlFor. Nothing is wrong in the initial HTML; the duplicate only exists in the RSC response for a later navigation, so it never shows up on a hard load.

The duplicate is visible in that response directly. With the repro running, this asks for / as a navigation from /other:

TREE='%5B%22%22%2C%7B%22children%22%3A%5B%22other%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%2C4096%5D%7D%2Cnull%2Cnull%2C4096%5D%7D%2Cnull%2Cnull%2C4112%5D'
curl -sL -H 'RSC: 1' -H "Next-Router-State-Tree: $TREE" http://localhost:3000/ | grep -o '"id":"_S_[0-9a-z]*_"'
# "id":"_S_1_"   <- the page's pattern; the layout's pattern in the DOM is also _S_1_

Expected. Ids from useId() are unique across the document for the lifetime of the page, as they are for Client Components (server-rendered ids are tree-position based, _R_…_, and client-mounted ids use a separate _r_…_ counter, so those never collide).

Provide environment information
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.6.0: Fri Jul 31 19:19:08 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6050
  Available memory (MB): 65536
  Available CPU cores: 18
Binaries:
  Node: 24.19.0
  npm: 11.17.0
  Yarn: N/A
  pnpm: 11.24.0
Relevant Packages:
  next: 16.4.0-canary.20 // Latest available version is detected (16.4.0-canary.20).
  eslint-config-next: N/A
  react: 19.2.8
  react-dom: 19.2.8
  typescript: 5.9.3
Next.js Config:
  output: N/A
Which area(s) are affected? (Select all that apply)

Linking and Navigating, React

Which stage(s) are affected? (Select all that apply)

next dev (local), next start (local)

Additional context

Mechanism, read from the installed packages:

  • React Flight's useId (react-server-dom-turbopack and react-server-dom-webpack server bundles) is "_" + request.identifierPrefix + "S_" + (request.identifierCount++).toString(32) + "_", with identifierCount initialised to 1 in the Request constructor. It has no tree-position component, so uniqueness holds only within one Flight response.
  • React designed it that way and left the rest to the caller. From react/react#24172, which added useId to Flight: "since Flight does support refetching subtrees it is possible a client will need to patch up a part of the tree rather than replacing the entire thing so it is not safe to use a simple incrementing counter. To solve for this we allow the caller to specify a prefix. On an initial fetch it is likely this will be empty but on refetches or subtrees we expect to have a client useId provide the prefix since it will guaranteed be unique for that subtree and thus for the entire tree."
  • Next.js never supplies that prefix. Every Flight render in packages/next/src/server/app-render goes through renderToWebFlightStream / renderToNodeFlightStream (stream-ops.web.ts / stream-ops.node.ts), and no options object passed to them contains identifierPrefix; a grep for it under next/dist/server finds nothing in 16.2.10, 16.3.4 or 16.4.0-canary.20. Meanwhile walkTreeWithFlightRouterState skips the segments that match the client's router state tree, so a navigation response contains only the changed subtree while the DOM keeps the previous response's ids.

A fix on the Next.js side could pass a per-response identifierPrefix to the Flight renderer for navigation (and Server Action) requests, for example a request counter or the request id, so ids from different responses cannot collide. Failing that, documenting that useId in Server Components is not document-unique across navigations would save people the debugging.

How I hit it: a Server Component in a root layout drew a background texture with an SVG <pattern id={useId()}>, and a page's dot-screen texture rendered as the layout's hatching after navigating to it. Workaround in app code: name such ids after what they identify, or move the component to "use client".

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.