Skip to content

Commit 8c598fb

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: b3e3b82 Pull Request resolved: #30889
1 parent a06cd9e commit 8c598fb

File tree

2 files changed

+27
-13
lines changed

2 files changed

+27
-13
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,9 @@ function renderWithHooksAgain<Props, SecondArg>(
828828
currentHook = null;
829829
workInProgressHook = null;
830830

831-
workInProgress.updateQueue = null;
831+
if (workInProgress.updateQueue != null) {
832+
clearFunctionComponentUpdateQueue(workInProgress.updateQueue);
833+
}
832834

833835
if (__DEV__) {
834836
// Also validate hook order for cascading updates.
@@ -1101,6 +1103,20 @@ if (enableUseMemoCacheHook) {
11011103
};
11021104
}
11031105

1106+
// NOTE: this function intentionally does not reset memoCache. We reuse updateQueue for the memo
1107+
// cache to avoid increasing the size of fibers that don't need a cache, but we don't want to reset
1108+
// the cache when other properties are reset.
1109+
const clearFunctionComponentUpdateQueue = (
1110+
updateQueue: FunctionComponentUpdateQueue,
1111+
) => {
1112+
updateQueue.lastEffect = null;
1113+
updateQueue.events = null;
1114+
updateQueue.stores = null;
1115+
if (updateQueue.memoCache != null) {
1116+
updateQueue.memoCache.index = 0;
1117+
}
1118+
};
1119+
11041120
function useThenable<T>(thenable: Thenable<T>): T {
11051121
// Track the position of the thenable within this fiber.
11061122
const index = thenableIndexCounter;

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

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ describe('useMemoCache()', () => {
667667
}
668668

669669
// Baseline / source code
670-
function useUserMemo(value) {
670+
function useManualMemo(value) {
671671
return useMemo(() => [value], [value]);
672672
}
673673

@@ -683,24 +683,22 @@ describe('useMemoCache()', () => {
683683
}
684684

685685
/**
686-
* Test case: note that the initial render never completes
686+
* Test with useMemoCache
687687
*/
688688
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-
);
689+
const CompilerMemoComponent = makeComponent(useCompilerMemo);
690+
await act(() => {
691+
root.render(<CompilerMemoComponent value={2} />);
692+
});
693+
expect(root).toMatchRenderedOutput(<div>2</div>);
695694

696695
/**
697-
* Baseline test: initial render is expected to complete after a retry
698-
* (triggered by the setState)
696+
* Test with useMemo
699697
*/
700698
root = ReactNoop.createRoot();
701-
const CorrectComponent = makeComponent(useUserMemo);
699+
const HookMemoComponent = makeComponent(useManualMemo);
702700
await act(() => {
703-
root.render(<CorrectComponent value={2} />);
701+
root.render(<HookMemoComponent value={2} />);
704702
});
705703
expect(root).toMatchRenderedOutput(<div>2</div>);
706704
});

0 commit comments

Comments
 (0)