Skip to content

Commit 6a1722d

Browse files
committed
[compiler][runtime] repro: infinite render with useMemoCache + render phase updates
1 parent 61739a8 commit 6a1722d

File tree

1 file changed

+59
-0
lines changed

1 file changed

+59
-0
lines changed

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ let ReactNoop;
1313
let Scheduler;
1414
let act;
1515
let assertLog;
16+
let useMemo;
1617
let useState;
1718
let useMemoCache;
19+
let waitForThrow;
1820
let MemoCacheSentinel;
1921
let ErrorBoundary;
2022

@@ -27,8 +29,10 @@ describe('useMemoCache()', () => {
2729
Scheduler = require('scheduler');
2830
act = require('internal-test-utils').act;
2931
assertLog = require('internal-test-utils').assertLog;
32+
useMemo = React.useMemo;
3033
useMemoCache = require('react/compiler-runtime').c;
3134
useState = React.useState;
35+
waitForThrow = require('internal-test-utils').waitForThrow;
3236
MemoCacheSentinel = Symbol.for('react.memo_cache_sentinel');
3337

3438
class _ErrorBoundary extends React.Component {
@@ -621,4 +625,59 @@ describe('useMemoCache()', () => {
621625
</>,
622626
);
623627
});
628+
629+
// @gate enableUseMemoCacheHook
630+
it('(repro) infinite renders when used with setState during render', async () => {
631+
// Output of react compiler on `useUserMemo`
632+
function useCompilerMemo(value) {
633+
let arr;
634+
const $ = useMemoCache(2);
635+
if ($[0] !== value) {
636+
arr = [value];
637+
$[0] = value;
638+
$[1] = arr;
639+
} else {
640+
arr = $[1];
641+
}
642+
return arr;
643+
}
644+
645+
// Baseline / source code
646+
function useUserMemo(value) {
647+
return useMemo(() => [value], [value]);
648+
}
649+
650+
function makeComponent(hook) {
651+
return function Component({value}) {
652+
const state = hook(value);
653+
const [prevState, setPrevState] = useState(null);
654+
if (state !== prevState) {
655+
setPrevState(state);
656+
}
657+
return <div>{state.join(',')}</div>;
658+
};
659+
}
660+
661+
/**
662+
* Test case: note that the initial render never completes
663+
*/
664+
let root = ReactNoop.createRoot();
665+
const IncorrectInfiniteComponent = makeComponent(useCompilerMemo);
666+
root.render(<IncorrectInfiniteComponent value={2} />);
667+
await waitForThrow(
668+
'Too many re-renders. React limits the number of renders to prevent ' +
669+
'an infinite loop.',
670+
);
671+
672+
/**
673+
* Baseline test: initial render is expected to complete after a retry
674+
* (triggered by the setState)
675+
*/
676+
root = ReactNoop.createRoot();
677+
const CorrectComponent = makeComponent(useUserMemo);
678+
await act(() => {
679+
root.render(<CorrectComponent value={2} />);
680+
});
681+
expect(root).toMatchRenderedOutput(<div>2</div>);
682+
});
624683
});

0 commit comments

Comments
 (0)