`useId()` in Server Components returns duplicate ids after a client-side navigation
@icyJoseph is already working on this.
Since Sep 8, 2026.
- 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
npm install && npm run dev- Open http://localhost:3000/other (a hard load).
- Click Back to / (a
next/linkclient-side navigation). - 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-turbopackandreact-server-dom-webpackserver bundles) is"_" + request.identifierPrefix + "S_" + (request.identifierCount++).toString(32) + "_", withidentifierCountinitialised to 1 in theRequestconstructor. 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
useIdto 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 clientuseIdprovide 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-rendergoes throughrenderToWebFlightStream/renderToNodeFlightStream(stream-ops.web.ts/stream-ops.node.ts), and no options object passed to them containsidentifierPrefix; a grep for it undernext/dist/serverfinds nothing in 16.2.10, 16.3.4 or 16.4.0-canary.20. MeanwhilewalkTreeWithFlightRouterStateskips 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.