Skip to content

Commit 031abd2

Browse files
authored
Add warning and test for useSyncExternalStore when getSnapshot isn't cached (#22262)
* add warning and test * Wrap console error in __DEV__ flag * prettier
1 parent e07039b commit 031abd2

File tree

2 files changed

+37
-0
lines changed

2 files changed

+37
-0
lines changed

packages/use-sync-external-store/src/__tests__/useSyncExternalStoreShared-test.js

+26
Original file line numberDiff line numberDiff line change
@@ -618,4 +618,30 @@ describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
618618
expect(root).toMatchRenderedOutput('A1B1');
619619
});
620620
});
621+
622+
test('Infinite loop if getSnapshot keeps returning new reference', () => {
623+
const store = createExternalStore({});
624+
625+
function App() {
626+
const text = useSyncExternalStore(store.subscribe, () => ({}));
627+
return <Text text={JSON.stringify(text)} />;
628+
}
629+
630+
spyOnDev(console, 'error');
631+
632+
expect(() => {
633+
act(() => {
634+
createRoot(<App />);
635+
});
636+
}).toThrow(
637+
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
638+
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
639+
'the number of nested updates to prevent infinite loops.',
640+
);
641+
if (__DEV__) {
642+
expect(console.error.calls.argsFor(0)[0]).toMatch(
643+
'The result of getSnapshot should be cached to avoid an infinite loop',
644+
);
645+
}
646+
});
621647
});

packages/use-sync-external-store/src/useSyncExternalStore.js

+11
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const useSyncExternalStore =
2929
builtInAPI !== undefined ? builtInAPI : useSyncExternalStore_shim;
3030

3131
let didWarnOld18Alpha = false;
32+
let didWarnUncachedGetSnapshot = false;
3233

3334
// Disclaimer: This shim breaks many of the rules of React, and only works
3435
// because of a very particular set of implementation details and assumptions
@@ -63,6 +64,16 @@ function useSyncExternalStore_shim<T>(
6364
// implementation details, most importantly that updates are
6465
// always synchronous.
6566
const value = getSnapshot();
67+
if (__DEV__) {
68+
if (!didWarnUncachedGetSnapshot) {
69+
if (value !== getSnapshot()) {
70+
console.error(
71+
'The result of getSnapshot should be cached to avoid an infinite loop',
72+
);
73+
didWarnUncachedGetSnapshot = true;
74+
}
75+
}
76+
}
6677

6778
// Because updates are synchronous, we don't queue them. Instead we force a
6879
// re-render whenever the subscribed state changes by updating an some

0 commit comments

Comments
 (0)