Skip to content

Commit 15557fa

Browse files
authored
[Fix] properly track useId use in StrictMode in development (#25713)
In `<StrictMode>` in dev hooks are run twice on each render. For `useId` the re-render pass uses the `updateId` implementation rather than `mountId`. In the update path we don't increment the local id counter. This causes the render to look like no id was used which changes the tree context and leads to a different set of IDs being generated for subsequent calls to `useId` in the subtree. This was discovered here: vercel/next.js#43033 It was causing a hydration error because the ID generation no longer matched between server and client. When strict mode is off this does not happen because the hooks are only run once during hydration and it properly sees that the component did generate an ID. The fix is to not reset the localIdCounter in `renderWithHooksAgain`. It gets reset anyway once the `renderWithHooks` is complete and since we do not re-mount the ID in the `...Again` pass we should retain the state from the initial pass.
1 parent 8a23def commit 15557fa

File tree

3 files changed

+64
-2
lines changed

3 files changed

+64
-2
lines changed

packages/react-dom/src/__tests__/ReactDOMUseId-test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,4 +633,68 @@ describe('useId', () => {
633633
</div>
634634
`);
635635
});
636+
637+
// https://github.com/vercel/next.js/issues/43033
638+
// re-rendering in strict mode caused the localIdCounter to be reset but it the rerender hook does not
639+
// increment it again. This only shows up as a problem for subsequent useId's because it affects child
640+
// and sibling counters not the initial one
641+
it('does not forget it mounted an id when re-rendering in dev', async () => {
642+
function Parent() {
643+
const id = useId();
644+
return (
645+
<div>
646+
{id} <Child />
647+
</div>
648+
);
649+
}
650+
function Child() {
651+
const id = useId();
652+
return <div>{id}</div>;
653+
}
654+
655+
function App({showMore}) {
656+
return (
657+
<React.StrictMode>
658+
<Parent />
659+
</React.StrictMode>
660+
);
661+
}
662+
663+
await serverAct(async () => {
664+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
665+
pipe(writable);
666+
});
667+
expect(container).toMatchInlineSnapshot(`
668+
<div
669+
id="container"
670+
>
671+
<div>
672+
:R0:
673+
<!-- -->
674+
675+
<div>
676+
:R7:
677+
</div>
678+
</div>
679+
</div>
680+
`);
681+
682+
await clientAct(async () => {
683+
ReactDOMClient.hydrateRoot(container, <App />);
684+
});
685+
expect(container).toMatchInlineSnapshot(`
686+
<div
687+
id="container"
688+
>
689+
<div>
690+
:R0:
691+
<!-- -->
692+
693+
<div>
694+
:R7:
695+
</div>
696+
</div>
697+
</div>
698+
`);
699+
});
636700
});

packages/react-reconciler/src/ReactFiberHooks.new.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,6 @@ function renderWithHooksAgain<Props, SecondArg>(
697697
let children;
698698
do {
699699
didScheduleRenderPhaseUpdateDuringThisPass = false;
700-
localIdCounter = 0;
701700
thenableIndexCounter = 0;
702701

703702
if (numberOfReRenders >= RE_RENDER_LIMIT) {

packages/react-reconciler/src/ReactFiberHooks.old.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,6 @@ function renderWithHooksAgain<Props, SecondArg>(
697697
let children;
698698
do {
699699
didScheduleRenderPhaseUpdateDuringThisPass = false;
700-
localIdCounter = 0;
701700
thenableIndexCounter = 0;
702701

703702
if (numberOfReRenders >= RE_RENDER_LIMIT) {

0 commit comments

Comments
 (0)