Skip to content

Commit 1b55c98

Browse files
committed
Fix useMemoCache with setState in render
Fixes the bug that @alexmckenley and @mofeiZ found where setState-in-render can reset useMemoCache and cause an infinite loop. The bug was that renderWithHooksAgain() was not resetting hook state when rerendering (so useMemo values were preserved) but was resetting the updateQueue. This meant that the entire memo cache was cleared on a setState-in-render. The fix here is to call a new helper function to clear the update queue. It nulls out other properties, but for memoCache it just sets the index back to zero. ghstack-source-id: 57608c8 Pull Request resolved: #30889
1 parent a06cd9e commit 1b55c98

File tree

2 files changed

+41
-24
lines changed

2 files changed

+41
-24
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,7 @@ export function replaySuspendedComponentWithHooks<Props, SecondArg>(
770770
ignorePreviousDependencies =
771771
current !== null && current.type !== workInProgress.type;
772772
}
773+
workInProgress.updateQueue = null;
773774
const children = renderWithHooksAgain(
774775
workInProgress,
775776
Component,
@@ -828,7 +829,9 @@ function renderWithHooksAgain<Props, SecondArg>(
828829
currentHook = null;
829830
workInProgressHook = null;
830831

831-
workInProgress.updateQueue = null;
832+
if (workInProgress.updateQueue != null) {
833+
resetFunctionComponentUpdateQueue(workInProgress.updateQueue);
834+
}
832835

833836
if (__DEV__) {
834837
// Also validate hook order for cascading updates.
@@ -1101,6 +1104,22 @@ if (enableUseMemoCacheHook) {
11011104
};
11021105
}
11031106

1107+
function resetFunctionComponentUpdateQueue(
1108+
updateQueue: FunctionComponentUpdateQueue,
1109+
): void {
1110+
updateQueue.lastEffect = null;
1111+
updateQueue.events = null;
1112+
updateQueue.stores = null;
1113+
if (enableUseMemoCacheHook) {
1114+
if (updateQueue.memoCache != null) {
1115+
// NOTE: this function intentionally does not reset memoCache data. We reuse updateQueue for the memo
1116+
// cache to avoid increasing the size of fibers that don't need a cache, but we don't want to reset
1117+
// the cache when other properties are reset.
1118+
updateQueue.memoCache.index = 0;
1119+
}
1120+
}
1121+
}
1122+
11041123
function useThenable<T>(thenable: Thenable<T>): T {
11051124
// Track the position of the thenable within this fiber.
11061125
const index = thenableIndexCounter;
@@ -2496,17 +2515,15 @@ function pushEffect(
24962515
if (componentUpdateQueue === null) {
24972516
componentUpdateQueue = createFunctionComponentUpdateQueue();
24982517
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
2518+
}
2519+
const lastEffect = componentUpdateQueue.lastEffect;
2520+
if (lastEffect === null) {
24992521
componentUpdateQueue.lastEffect = effect.next = effect;
25002522
} else {
2501-
const lastEffect = componentUpdateQueue.lastEffect;
2502-
if (lastEffect === null) {
2503-
componentUpdateQueue.lastEffect = effect.next = effect;
2504-
} else {
2505-
const firstEffect = lastEffect.next;
2506-
lastEffect.next = effect;
2507-
effect.next = firstEffect;
2508-
componentUpdateQueue.lastEffect = effect;
2509-
}
2523+
const firstEffect = lastEffect.next;
2524+
lastEffect.next = effect;
2525+
effect.next = firstEffect;
2526+
componentUpdateQueue.lastEffect = effect;
25102527
}
25112528
return effect;
25122529
}

packages/react-reconciler/src/__tests__/useMemoCache-test.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ let assertLog;
1616
let useMemo;
1717
let useState;
1818
let useMemoCache;
19-
let waitForThrow;
2019
let MemoCacheSentinel;
2120
let ErrorBoundary;
2221

@@ -32,7 +31,6 @@ describe('useMemoCache()', () => {
3231
useMemo = React.useMemo;
3332
useMemoCache = require('react/compiler-runtime').c;
3433
useState = React.useState;
35-
waitForThrow = require('internal-test-utils').waitForThrow;
3634
MemoCacheSentinel = Symbol.for('react.memo_cache_sentinel');
3735

3836
class _ErrorBoundary extends React.Component {
@@ -600,6 +598,8 @@ describe('useMemoCache()', () => {
600598
await act(() => setInput('hi!'));
601599

602600
// Once the input has updated, we go back to rendering the transition.
601+
// We did not have process the first chunk again. We reused the
602+
// computation from the earlier attempt.
603603
if (gate(flags => flags.enableNoCloningMemoCache)) {
604604
// We did not have process the first chunk again. We reused the
605605
// computation from the earlier attempt.
@@ -630,6 +630,8 @@ describe('useMemoCache()', () => {
630630

631631
// Finish loading the data.
632632
await act(() => updatedChunkB.resolve('B2'));
633+
// We did not have process the first chunk again. We reused the
634+
// computation from the earlier attempt.
633635
if (gate(flags => flags.enableNoCloningMemoCache)) {
634636
// We did not have process the first chunk again. We reused the
635637
// computation from the earlier attempt.
@@ -667,7 +669,7 @@ describe('useMemoCache()', () => {
667669
}
668670

669671
// Baseline / source code
670-
function useUserMemo(value) {
672+
function useManualMemo(value) {
671673
return useMemo(() => [value], [value]);
672674
}
673675

@@ -683,24 +685,22 @@ describe('useMemoCache()', () => {
683685
}
684686

685687
/**
686-
* Test case: note that the initial render never completes
688+
* Test with useMemoCache
687689
*/
688690
let root = ReactNoop.createRoot();
689-
const IncorrectInfiniteComponent = makeComponent(useCompilerMemo);
690-
root.render(<IncorrectInfiniteComponent value={2} />);
691-
await waitForThrow(
692-
'Too many re-renders. React limits the number of renders to prevent ' +
693-
'an infinite loop.',
694-
);
691+
const CompilerMemoComponent = makeComponent(useCompilerMemo);
692+
await act(() => {
693+
root.render(<CompilerMemoComponent value={2} />);
694+
});
695+
expect(root).toMatchRenderedOutput(<div>2</div>);
695696

696697
/**
697-
* Baseline test: initial render is expected to complete after a retry
698-
* (triggered by the setState)
698+
* Test with useMemo
699699
*/
700700
root = ReactNoop.createRoot();
701-
const CorrectComponent = makeComponent(useUserMemo);
701+
const HookMemoComponent = makeComponent(useManualMemo);
702702
await act(() => {
703-
root.render(<CorrectComponent value={2} />);
703+
root.render(<HookMemoComponent value={2} />);
704704
});
705705
expect(root).toMatchRenderedOutput(<div>2</div>);
706706
});

0 commit comments

Comments
 (0)