Skip to content

Commit de5f63f

Browse files
committed
Fix: useId in strict mode
In strict mode, `renderWithHooks` is called twice to flush out side effects. Modying the tree context (`pushTreeId` and `pushTreeFork`) is effectful, so before this fix, the tree context was allocating two slots for a materialized id instead of one. To address, I lifted those calls outside of `renderWithHooks`. This is how I had originally structured it, and it's how Fizz is structured, too. The other solution would be to reset the stack in between the calls but that's also a bit weird because we usually only ever reset the stack during unwind or complete.
1 parent 9fb3442 commit de5f63f

File tree

8 files changed

+144
-36
lines changed

8 files changed

+144
-36
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,35 @@ describe('useId', () => {
198198
`);
199199
});
200200

201+
test('StrictMode double rendering', async () => {
202+
const {StrictMode} = React;
203+
204+
function App() {
205+
return (
206+
<StrictMode>
207+
<DivWithId />
208+
</StrictMode>
209+
);
210+
}
211+
212+
await serverAct(async () => {
213+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
214+
pipe(writable);
215+
});
216+
await clientAct(async () => {
217+
ReactDOM.hydrateRoot(container, <App />);
218+
});
219+
expect(container).toMatchInlineSnapshot(`
220+
<div
221+
id="container"
222+
>
223+
<div
224+
id="0"
225+
/>
226+
</div>
227+
`);
228+
});
229+
201230
test('empty (null) children', async () => {
202231
// We don't treat empty children different from non-empty ones, which means
203232
// they get allocated a slot when generating ids. There's no inherent reason

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,11 @@ import {
174174
prepareToReadContext,
175175
scheduleWorkOnParentPath,
176176
} from './ReactFiberNewContext.new';
177-
import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.new';
177+
import {
178+
renderWithHooks,
179+
checkDidRenderIdHook,
180+
bailoutHooks,
181+
} from './ReactFiberHooks.new';
178182
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.new';
179183
import {
180184
getMaskedContext,
@@ -240,6 +244,7 @@ import {
240244
getForksAtLevel,
241245
isForkedChild,
242246
pushTreeId,
247+
pushMaterializedTreeId,
243248
} from './ReactFiberTreeContext.new';
244249

245250
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -365,6 +370,7 @@ function updateForwardRef(
365370

366371
// The rest is a fork of updateFunctionComponent
367372
let nextChildren;
373+
let hasId;
368374
prepareToReadContext(workInProgress, renderLanes);
369375
if (enableSchedulingProfiler) {
370376
markComponentRenderStarted(workInProgress);
@@ -380,6 +386,7 @@ function updateForwardRef(
380386
ref,
381387
renderLanes,
382388
);
389+
hasId = checkDidRenderIdHook();
383390
if (
384391
debugRenderPhaseSideEffectsForStrictMode &&
385392
workInProgress.mode & StrictLegacyMode
@@ -394,6 +401,7 @@ function updateForwardRef(
394401
ref,
395402
renderLanes,
396403
);
404+
hasId = checkDidRenderIdHook();
397405
} finally {
398406
setIsStrictModeForDevtools(false);
399407
}
@@ -408,6 +416,7 @@ function updateForwardRef(
408416
ref,
409417
renderLanes,
410418
);
419+
hasId = checkDidRenderIdHook();
411420
}
412421
if (enableSchedulingProfiler) {
413422
markComponentRenderStopped();
@@ -418,6 +427,10 @@ function updateForwardRef(
418427
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
419428
}
420429

430+
if (getIsHydrating() && hasId) {
431+
pushMaterializedTreeId(workInProgress);
432+
}
433+
421434
// React DevTools reads this flag.
422435
workInProgress.flags |= PerformedWork;
423436
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -970,6 +983,7 @@ function updateFunctionComponent(
970983
}
971984

972985
let nextChildren;
986+
let hasId;
973987
prepareToReadContext(workInProgress, renderLanes);
974988
if (enableSchedulingProfiler) {
975989
markComponentRenderStarted(workInProgress);
@@ -985,6 +999,7 @@ function updateFunctionComponent(
985999
context,
9861000
renderLanes,
9871001
);
1002+
hasId = checkDidRenderIdHook();
9881003
if (
9891004
debugRenderPhaseSideEffectsForStrictMode &&
9901005
workInProgress.mode & StrictLegacyMode
@@ -999,6 +1014,7 @@ function updateFunctionComponent(
9991014
context,
10001015
renderLanes,
10011016
);
1017+
hasId = checkDidRenderIdHook();
10021018
} finally {
10031019
setIsStrictModeForDevtools(false);
10041020
}
@@ -1013,6 +1029,7 @@ function updateFunctionComponent(
10131029
context,
10141030
renderLanes,
10151031
);
1032+
hasId = checkDidRenderIdHook();
10161033
}
10171034
if (enableSchedulingProfiler) {
10181035
markComponentRenderStopped();
@@ -1023,6 +1040,10 @@ function updateFunctionComponent(
10231040
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
10241041
}
10251042

1043+
if (getIsHydrating() && hasId) {
1044+
pushMaterializedTreeId(workInProgress);
1045+
}
1046+
10261047
// React DevTools reads this flag.
10271048
workInProgress.flags |= PerformedWork;
10281049
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -1593,6 +1614,7 @@ function mountIndeterminateComponent(
15931614

15941615
prepareToReadContext(workInProgress, renderLanes);
15951616
let value;
1617+
let hasId;
15961618

15971619
if (enableSchedulingProfiler) {
15981620
markComponentRenderStarted(workInProgress);
@@ -1629,6 +1651,7 @@ function mountIndeterminateComponent(
16291651
context,
16301652
renderLanes,
16311653
);
1654+
hasId = checkDidRenderIdHook();
16321655
setIsRendering(false);
16331656
} else {
16341657
value = renderWithHooks(
@@ -1639,6 +1662,7 @@ function mountIndeterminateComponent(
16391662
context,
16401663
renderLanes,
16411664
);
1665+
hasId = checkDidRenderIdHook();
16421666
}
16431667
if (enableSchedulingProfiler) {
16441668
markComponentRenderStopped();
@@ -1758,12 +1782,17 @@ function mountIndeterminateComponent(
17581782
context,
17591783
renderLanes,
17601784
);
1785+
hasId = checkDidRenderIdHook();
17611786
} finally {
17621787
setIsStrictModeForDevtools(false);
17631788
}
17641789
}
17651790
}
17661791

1792+
if (getIsHydrating() && hasId) {
1793+
pushMaterializedTreeId(workInProgress);
1794+
}
1795+
17671796
reconcileChildren(null, workInProgress, value, renderLanes);
17681797
if (__DEV__) {
17691798
validateFunctionComponentInDev(workInProgress, Component);

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,11 @@ import {
174174
prepareToReadContext,
175175
scheduleWorkOnParentPath,
176176
} from './ReactFiberNewContext.old';
177-
import {renderWithHooks, bailoutHooks} from './ReactFiberHooks.old';
177+
import {
178+
renderWithHooks,
179+
checkDidRenderIdHook,
180+
bailoutHooks,
181+
} from './ReactFiberHooks.old';
178182
import {stopProfilerTimerIfRunning} from './ReactProfilerTimer.old';
179183
import {
180184
getMaskedContext,
@@ -240,6 +244,7 @@ import {
240244
getForksAtLevel,
241245
isForkedChild,
242246
pushTreeId,
247+
pushMaterializedTreeId,
243248
} from './ReactFiberTreeContext.old';
244249

245250
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -365,6 +370,7 @@ function updateForwardRef(
365370

366371
// The rest is a fork of updateFunctionComponent
367372
let nextChildren;
373+
let hasId;
368374
prepareToReadContext(workInProgress, renderLanes);
369375
if (enableSchedulingProfiler) {
370376
markComponentRenderStarted(workInProgress);
@@ -380,6 +386,7 @@ function updateForwardRef(
380386
ref,
381387
renderLanes,
382388
);
389+
hasId = checkDidRenderIdHook();
383390
if (
384391
debugRenderPhaseSideEffectsForStrictMode &&
385392
workInProgress.mode & StrictLegacyMode
@@ -394,6 +401,7 @@ function updateForwardRef(
394401
ref,
395402
renderLanes,
396403
);
404+
hasId = checkDidRenderIdHook();
397405
} finally {
398406
setIsStrictModeForDevtools(false);
399407
}
@@ -408,6 +416,7 @@ function updateForwardRef(
408416
ref,
409417
renderLanes,
410418
);
419+
hasId = checkDidRenderIdHook();
411420
}
412421
if (enableSchedulingProfiler) {
413422
markComponentRenderStopped();
@@ -418,6 +427,10 @@ function updateForwardRef(
418427
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
419428
}
420429

430+
if (getIsHydrating() && hasId) {
431+
pushMaterializedTreeId(workInProgress);
432+
}
433+
421434
// React DevTools reads this flag.
422435
workInProgress.flags |= PerformedWork;
423436
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -970,6 +983,7 @@ function updateFunctionComponent(
970983
}
971984

972985
let nextChildren;
986+
let hasId;
973987
prepareToReadContext(workInProgress, renderLanes);
974988
if (enableSchedulingProfiler) {
975989
markComponentRenderStarted(workInProgress);
@@ -985,6 +999,7 @@ function updateFunctionComponent(
985999
context,
9861000
renderLanes,
9871001
);
1002+
hasId = checkDidRenderIdHook();
9881003
if (
9891004
debugRenderPhaseSideEffectsForStrictMode &&
9901005
workInProgress.mode & StrictLegacyMode
@@ -999,6 +1014,7 @@ function updateFunctionComponent(
9991014
context,
10001015
renderLanes,
10011016
);
1017+
hasId = checkDidRenderIdHook();
10021018
} finally {
10031019
setIsStrictModeForDevtools(false);
10041020
}
@@ -1013,6 +1029,7 @@ function updateFunctionComponent(
10131029
context,
10141030
renderLanes,
10151031
);
1032+
hasId = checkDidRenderIdHook();
10161033
}
10171034
if (enableSchedulingProfiler) {
10181035
markComponentRenderStopped();
@@ -1023,6 +1040,10 @@ function updateFunctionComponent(
10231040
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
10241041
}
10251042

1043+
if (getIsHydrating() && hasId) {
1044+
pushMaterializedTreeId(workInProgress);
1045+
}
1046+
10261047
// React DevTools reads this flag.
10271048
workInProgress.flags |= PerformedWork;
10281049
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
@@ -1593,6 +1614,7 @@ function mountIndeterminateComponent(
15931614

15941615
prepareToReadContext(workInProgress, renderLanes);
15951616
let value;
1617+
let hasId;
15961618

15971619
if (enableSchedulingProfiler) {
15981620
markComponentRenderStarted(workInProgress);
@@ -1629,6 +1651,7 @@ function mountIndeterminateComponent(
16291651
context,
16301652
renderLanes,
16311653
);
1654+
hasId = checkDidRenderIdHook();
16321655
setIsRendering(false);
16331656
} else {
16341657
value = renderWithHooks(
@@ -1639,6 +1662,7 @@ function mountIndeterminateComponent(
16391662
context,
16401663
renderLanes,
16411664
);
1665+
hasId = checkDidRenderIdHook();
16421666
}
16431667
if (enableSchedulingProfiler) {
16441668
markComponentRenderStopped();
@@ -1758,12 +1782,17 @@ function mountIndeterminateComponent(
17581782
context,
17591783
renderLanes,
17601784
);
1785+
hasId = checkDidRenderIdHook();
17611786
} finally {
17621787
setIsStrictModeForDevtools(false);
17631788
}
17641789
}
17651790
}
17661791

1792+
if (getIsHydrating() && hasId) {
1793+
pushMaterializedTreeId(workInProgress);
1794+
}
1795+
17671796
reconcileChildren(null, workInProgress, value, renderLanes);
17681797
if (__DEV__) {
17691798
validateFunctionComponentInDev(workInProgress, Component);

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

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ import {
110110
} from './ReactUpdateQueue.new';
111111
import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new';
112112
import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags';
113-
import {getTreeId, pushTreeFork, pushTreeId} from './ReactFiberTreeContext.new';
113+
import {getTreeId} from './ReactFiberTreeContext.new';
114114

115115
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
116116

@@ -432,6 +432,7 @@ export function renderWithHooks<Props, SecondArg>(
432432
let numberOfReRenders: number = 0;
433433
do {
434434
didScheduleRenderPhaseUpdateDuringThisPass = false;
435+
localIdCounter = 0;
435436

436437
if (numberOfReRenders >= RE_RENDER_LIMIT) {
437438
throw new Error(
@@ -513,6 +514,8 @@ export function renderWithHooks<Props, SecondArg>(
513514
}
514515

515516
didScheduleRenderPhaseUpdate = false;
517+
// This is reset by checkDidRenderIdHook
518+
// localIdCounter = 0;
516519

517520
if (didRenderTooFewHooks) {
518521
throw new Error(
@@ -541,25 +544,18 @@ export function renderWithHooks<Props, SecondArg>(
541544
}
542545
}
543546
}
544-
545-
if (localIdCounter !== 0) {
546-
localIdCounter = 0;
547-
if (getIsHydrating()) {
548-
// This component materialized an id. This will affect any ids that appear
549-
// in its children.
550-
const returnFiber = workInProgress.return;
551-
if (returnFiber !== null) {
552-
const numberOfForks = 1;
553-
const slotIndex = 0;
554-
pushTreeFork(workInProgress, numberOfForks);
555-
pushTreeId(workInProgress, numberOfForks, slotIndex);
556-
}
557-
}
558-
}
559-
560547
return children;
561548
}
562549

550+
export function checkDidRenderIdHook() {
551+
// This should be called immediately after every renderWithHooks call.
552+
// Conceptually, it's part of the return value of renderWithHooks; it's only a
553+
// separate function to avoid using an array tuple.
554+
const didRenderIdHook = localIdCounter !== 0;
555+
localIdCounter = 0;
556+
return didRenderIdHook;
557+
}
558+
563559
export function bailoutHooks(
564560
current: Fiber,
565561
workInProgress: Fiber,

0 commit comments

Comments
 (0)