next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Hydration failure with `useId()` and AG Grid Enterprise

Open rwalisa opened this issue 3 months ago • 21 comments

Link to the code that reproduces this issue

https://github.com/rwalisa/nextjs-useid-hydration-bug-demo/

To Reproduce

  1. Start the application in development, or build and run the server
  2. Open the index page
  3. In case the error doesn't appear, refresh it
  4. Remove line 7 in Component.jsx
  5. 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.

rwalisa avatar Sep 20 '25 23:09 rwalisa

On my laptop, this happens on every page refresh in Firefox, once every few refreshes in Safari and almost never in Chrome.

rwalisa avatar Sep 20 '25 23:09 rwalisa

I tried it but everything seems fine.

vikas-saini-7 avatar Sep 21 '25 17:09 vikas-saini-7

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 avatar Sep 22 '25 21:09 icyJoseph

@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...

rwalisa avatar Sep 23 '25 00:09 rwalisa

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.

icyJoseph avatar Sep 23 '25 10:09 icyJoseph

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.

ZeroWave022 avatar Oct 17 '25 21:10 ZeroWave022

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.

rwalisa avatar Oct 19 '25 01:10 rwalisa

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.

icyJoseph avatar Oct 20 '25 08:10 icyJoseph

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.

ZeroWave022 avatar Oct 20 '25 21:10 ZeroWave022

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.

zzswang avatar Oct 26 '25 13:10 zzswang

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

eli-front avatar Oct 29 '25 21:10 eli-front

15.4.2 has major security vuln ...

ybelakov avatar Oct 30 '25 05:10 ybelakov

Has anyone been able to create a simple repro? The problem with the ag-grid repro, is that, ag-grid itself is rather large.

icyJoseph avatar Oct 30 '25 08:10 icyJoseph

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.

eli-front avatar Oct 31 '25 17:10 eli-front

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!

ybelakov avatar Oct 31 '25 21:10 ybelakov

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.

AlessioGr avatar Nov 03 '25 19:11 AlessioGr

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.

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;
}

eli-front avatar Nov 05 '25 18:11 eli-front

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.

julienchabanon avatar Nov 11 '25 11:11 julienchabanon

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.js 15.4.7 is 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.

AlessioGr avatar Nov 22 '25 00:11 AlessioGr

whats the status here? i am hitting this a lot

ybelakov avatar Nov 22 '25 01:11 ybelakov

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.js 15.4.7 is 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?

eli-front avatar Nov 25 '25 19:11 eli-front