Hydration failure with `useId()` and AG Grid Enterprise
Link to the code that reproduces this issue
https://github.com/rwalisa/nextjs-useid-hydration-bug-demo/
To Reproduce
- Start the application in development, or build and run the server
- Open the index page
- In case the error doesn't appear, refresh it
- Remove line 7 in
Component.jsx - Refresh the page to see that the error is gone
Current vs. Expected behavior
Hydration fails, because useId() returns _R_clrlb_ on the server and _R_4lrlb_ on the client.
Provide environment information
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.0.0: Mon Aug 25 21:16:39 PDT 2025; root:xnu-12377.1.9~3/RELEASE_ARM64_T6031
Available memory (MB): 36864
Available CPU cores: 14
Binaries:
Node: 24.8.0
npm: 11.6.0
Yarn: N/A
pnpm: N/A
Relevant Packages:
next: 15.5.3 // Latest available version is detected (15.5.3).
eslint-config-next: 15.5.3
react: 19.1.0
react-dom: 19.1.0
typescript: 5.9.2
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
React
Which stage(s) are affected? (Select all that apply)
next dev (local), next build (local), next start (local)
Additional context
The issue happens starting from Next.js v15.4.2-canary.20. v15.4.2-canary.19 works as expected. All patch versions in 15.5.x are affected.
It only happens when AG Grid Enterprise is imported in a client component (not necessarily the one that calls useId()) and referenced in the code by accessing any of its exports. Since the imported package doesn't import React, I think it's an issue with Next.js module resolution.
Importing AG Grid Community doesn't trigger the issue.
On my laptop, this happens on every page refresh in Firefox, once every few refreshes in Safari and almost never in Chrome.
I tried it but everything seems fine.
So- yeah, in Firefox and Safari it happens. Easily reproducible. I think I have a target commit from where this could've started, but it is still not useful because I can't pinpoint what kind of side-effect is happening that triggers this issue.
I thought it was some kind of license watermarking from the enterprise version that triggered this, by editing the DOM early under certain assumptions, but I haven't been able to demonstrate that.
I also removed the <div></div> from the page.js file, and then it worked fine... or wrapping Component in a span worked too. So there's something about the DOM structure, in combination with the ag-grid-enterprise full module that trips this... 🤔
I guess, one could git pull ag-grid-enterprise and try to build it locally, and reproduce the error, then start to delete stuff until it doesn't break. Editing node_modules could work too I suppose, but when it is about side-effects, things just get trickier. Assuming it is a side-effect too 🙃
@icyJoseph I thought that package was supposed to be side-effect-free (at least until you register the imported modules with their module registry), but I might be wrong about that. I tried creating a mutation observer on the document by adding this to layout, but it didn't show anything suspicious:
<script dangerouslySetInnerHTML={{ __html: `
function logChanges(records, observer) {
for (const record of records) {
console.log(record);
if (record.addedNodes.length > 0)
console.log("Added\\n" + [...record.addedNodes].map(n => n?.outerHTML ?? n?.data).join("\\n"));
if (record.removedNodes.length > 0)
console.log("Removed\\n" + [...record.removedNodes].map(n => n?.outerHTML ?? n?.data).join("\\n"));
}
}
const observerOptions = {
childList: true,
subtree: true,
};
const observer = new MutationObserver(logChanges);
observer.observe(document.documentElement, observerOptions);` }} />
If one doesn't set a license key, there's a watermark that appears on the table itself, along with a few console.error messages. But those are triggered by rendering the table, whereas this issue happens before that.
In the app where I first discovered it, the table component is wrapped in its own <div>, and it doesn't useId() (another component earlier in the tree does), so I'm not sure what exactly it is about the DOM structure that causes it. I thought it's unlikely to matter, since the imported script shouldn't be able to know which component it's imported from, but it does seem to affect it...
I have some ideas on what to do next, but those take time and I am bit busy right now.
Right now, ag-grid-enterprise maintainers might have a better, more efficient, chance at trying to narrow down what the issue really is. Let's hope they can take a look at it.
Based on what you're describing rwalisa, this is very similar to what I reported here: https://github.com/radix-ui/primitives/issues/3700. The difference being that Radix UI components use the useId hook in their source code directly (although with some custom modifications), and it fails with similar results like the ones you see.
If this is the same issue, it could be a hint that the issue isn't necessarily linked to one particular package, but rather a change in React's useId or alternatively something else in Next.js.
That does sound like the same issue. I thought it's more likely to be a bug in Next, not AG Grid, but was never able to pin down what exactly causes it. We ended up downgrading to ~15.4.
Yeah I never got around simplifying the repository to a clear code pattern that reliably triggers the issue. I had suspected perhaps multiple React versions were at fault, but could never prove it.
Is there a minimal repository for the Radix issue? @ZeroWave022 ? Also - the issues look similar, but we haven't yet boiled it all down to a given pattern, just yet.
I just tried to create a minimal reproduction demo, but couldn't find exactly what is causing the issue. Removing seemingly random parts of the code seems to reduce the probability of the error occurring or removing it completely. The project in which this issue comes up has a lot of dependencies, so that also makes it more difficult to pinpoint it.
It's worth mentioning that it doesn't happen on every single refresh, and for some reason seems to be happening more often on Firefox than on Chrome. Sorry that I couldn't help any more than that.
NextJs 16.0.0 React 19.2.0
Radix UI
Any component that uses Slot has a certain probability of experiencing this issue, such as Popover, DropdownMenu, Tooltip, etc. The longer the compilation time, the more likely errors will occur. The error probability is low, sometimes requiring dozens of refreshes to appear.
Seeing this with https://github.com/bvaughn/react-resizable-panels too with Next 16. Our temporary solution is to downgrade to next 15.4.2
Edit: don't use 15.4.2. Use 15.4.7
15.4.2 has major security vuln ...
Has anyone been able to create a simple repro? The problem with the ag-grid repro, is that, ag-grid itself is rather large.
Has anyone been able to create a simple repro? The problem with the ag-grid repro, is that, ag-grid itself is rather large.
For me the issue seems relatively inconsistent. Trying to find a stronger repro.
any update here?
Edit by maintainer bot: Comment was automatically minimized because it was considered unhelpful. (If you think this was by mistake, let us know). Please only comment if it adds context to the issue. If you want to express that you have the same problem, use the upvote 👍 on the issue description or subscribe to the issue for updates. Thanks!
We're experiencing this issue at Payload as well. We use dnd-kit, which adds an aria-describedBy attribute generated by useId. On some page loads (5% of times), it throws a hydration error.
We're experiencing this issue at Payload as well. We use dnd-kit, which adds an
aria-describedByattribute generated byuseId. On some page loads (5% of times), it throws a hydration error.
Discovery: dnd-kit, radix-ui, and react-resizable-panels use custom useId hooks. Might be related?
DnDKit
import {useMemo} from 'react';
let ids: Record<string, number> = {};
export function useUniqueId(prefix: string, value?: string) {
return useMemo(() => {
if (value) {
return value;
}
const id = ids[prefix] == null ? 0 : ids[prefix] + 1;
ids[prefix] = id;
return `${prefix}-${id}`;
}, [prefix, value]);
}
Radix UI
import * as React from 'react';
import { useLayoutEffect } from '@radix-ui/react-use-layout-effect';
// We spaces with `.trim().toString()` to prevent bundlers from trying to `import { useId } from 'react';`
const useReactId = (React as any)[' useId '.trim().toString()] || (() => undefined);
let count = 0;
function useId(deterministicId?: string): string {
const [id, setId] = React.useState<string | undefined>(useReactId());
// React versions older than 18 will have client-side ids only.
useLayoutEffect(() => {
if (!deterministicId) setId((reactId) => reactId ?? String(count++));
}, [deterministicId]);
return deterministicId || (id ? `radix-${id}` : '');
}
export { useId };
ReactResizablePanels
import * as React from "react";
import { useRef } from "react";
const useId = (React as any)["useId".toString()] as (() => string) | undefined;
const wrappedUseId: () => string | null =
typeof useId === "function" ? useId : (): null => null;
let counter = 0;
export default function useUniqueId(
idFromParams: string | null = null
): string {
const idFromUseId = wrappedUseId();
const idRef = useRef<string | null>(idFromParams || idFromUseId || null);
if (idRef.current === null) {
idRef.current = "" + counter++;
}
return idFromParams ?? idRef.current;
}
This issue also affects Radix, which is used by Shadcn, for example. So users like myself who rely on Shadcn have no choice but to pin Next.js to version 14.4.7 for now. The issue appears randomly, about 5% of the time in my testing, but opening the Chrome console seems to increase the likelihood of it occurring.
Just wanted to share my findings here in case it helps others debug:
- The issue was introduced in Next.js
15.4.2-canary.20. Next.js15.4.7is the last stable release without this issue. - It stems from changes in React DOM, introduced in facebook/react#34031. It appeared once Next.js upgraded its React version in that canary release.
- If you revert the changes in
node_modules/next/dist/compiled/react-dom/react-dom-client.development.js, the issue goes away.
whats the status here? i am hitting this a lot
Just wanted to share my findings here in case it helps others debug:
- The issue was introduced in Next.js
15.4.2-canary.20. Next.js15.4.7is the last stable release without this issue.- It stems from changes in React DOM, introduced in facebook/react#34031. It appeared once Next.js upgraded its React version in that canary release.
- If you revert the changes in
node_modules/next/dist/compiled/react-dom/react-dom-client.development.js, the issue goes away.
Do you have a patch file you could share?