/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @emails react-core*/'use strict';
let useSyncExternalStore;
let useSyncExternalStoreWithSelector;
let React;
let ReactDOM;
let ReactDOMClient;
let ReactFeatureFlags;
let Scheduler;
let act;
let useState;
let useEffect;
let useLayoutEffect;
let assertLog;
// This tests shared behavior between the built-in and shim implementations of// of useSyncExternalStore.describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
beforeEach(() => {
jest.resetModules();
if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
// Remove useSyncExternalStore from the React imports so that we use the
// shim instead. Also removing startTransition, since we use that to
// detect outdated 18 alphas that don't yet include useSyncExternalStore.
//
// Longer term, we'll probably test this branch using an actual build
// of React 17.
jest.mock('react', () => {
const {
// eslint-disable-next-line no-unused-vars
startTransition: _,
// eslint-disable-next-line no-unused-vars
useSyncExternalStore: __,
...otherExports
} = jest.requireActual('react');
return otherExports;
});}React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactFeatureFlags = require('shared/ReactFeatureFlags');
Scheduler = require('scheduler');
useState = React.useState;
useEffect = React.useEffect;
useLayoutEffect = React.useLayoutEffect;
const InternalTestUtils = require('internal-test-utils');
assertLog = InternalTestUtils.assertLog;
const internalAct = require('internal-test-utils').act;
// The internal act implementation doesn't batch updates by default, since
// it's mostly used to test concurrent mode. But since these tests run
// in both concurrent and legacy mode, I'm adding batching here.
act = cb => internalAct(() => ReactDOM.unstable_batchedUpdates(cb));
if (gate(flags => flags.source)) {
// The `shim/with-selector` module composes the main
// `use-sync-external-store` entrypoint. In the compiled artifacts, this
// is resolved to the `shim` implementation by our build config, but when
// running the tests against the source files, we need to tell Jest how to
// resolve it. Because this is a source module, this mock has no affect on
// the build tests.
jest.mock('use-sync-external-store/src/useSyncExternalStore', () =>
jest.requireActual('use-sync-external-store/shim'),
);}useSyncExternalStore =
require('use-sync-external-store/shim').useSyncExternalStore;
useSyncExternalStoreWithSelector =
require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
});function Text({text}) {
Scheduler.log(text);
return text;
}function createRoot(container) {
// This wrapper function exists so we can test both legacy roots and
// concurrent roots.
if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
// The native implementation only exists in 18+, so we test using
// concurrent mode. To test the legacy root behavior in the native
// implementation (which is supported in the sense that it needs to have
// the correct behavior, despite the fact that the legacy root API
// triggers a warning in 18), write a test that uses
// createLegacyRoot directly.
return ReactDOMClient.createRoot(container);
} else {
ReactDOM.render(null, container);
return {
render(children) {
ReactDOM.render(children, container);
},};}}function createExternalStore(initialState) {
const listeners = new Set();
let currentState = initialState;
return {
set(text) {
currentState = text;
ReactDOM.unstable_batchedUpdates(() => {
listeners.forEach(listener => listener());
});},subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},getState() {
return currentState;
},getSubscriberCount() {
return listeners.size;
},};}test('basic usage', async () => {
const store = createExternalStore('Initial');
function App() {
const text = useSyncExternalStore(store.subscribe, store.getState);
return <Text text={text} />;
}const container = document.createElement('div');
const root = createRoot(container);
await act(() => root.render(<App />));
assertLog(['Initial']);
expect(container.textContent).toEqual('Initial');
await act(() => {
store.set('Updated');
});assertLog(['Updated']);
expect(container.textContent).toEqual('Updated');
});test('skips re-rendering if nothing changes', async () => {
const store = createExternalStore('Initial');
function App() {
const text = useSyncExternalStore(store.subscribe, store.getState);
return <Text text={text} />;
}const container = document.createElement('div');
const root = createRoot(container);
await act(() => root.render(<App />));
assertLog(['Initial']);
expect(container.textContent).toEqual('Initial');
// Update to the same value
await act(() => {
store.set('Initial');
});// Should not re-render
assertLog([]);
expect(container.textContent).toEqual('Initial');
});test('switch to a different store', async () => {
const storeA = createExternalStore(0);
const storeB = createExternalStore(0);
let setStore;
function App() {
const [store, _setStore] = useState(storeA);
setStore = _setStore;
const value = useSyncExternalStore(store.subscribe, store.getState);
return <Text text={value} />;
}const container = document.createElement('div');
const root = createRoot(container);
await act(() => root.render(<App />));
assertLog([0]);
expect(container.textContent).toEqual('0');
await act(() => {
storeA.set(1);
});assertLog([1]);
expect(container.textContent).toEqual('1');
// Switch stores and update in the same batch
await act(() => {
ReactDOM.flushSync(() => {
// This update will be disregarded
storeA.set(2);
setStore(storeB);
});});// Now reading from B instead of A
assertLog([0]);
expect(container.textContent).toEqual('0');
// Update A
await act(() => {
storeA.set(3);
});// Nothing happened, because we're no longer subscribed to A
assertLog([]);
expect(container.textContent).toEqual('0');
// Update B
await act(() => {
storeB.set(1);
});assertLog([1]);
expect(container.textContent).toEqual('1');
});test('selecting a specific value inside getSnapshot', async () => {
const store = createExternalStore({a: 0, b: 0});
function A() {
const a = useSyncExternalStore(store.subscribe, () => store.getState().a);
return <Text text={'A' + a} />;
}function B() {
const b = useSyncExternalStore(store.subscribe, () => store.getState().b);
return <Text text={'B' + b} />;
}function App() {
return (
<><A />
<B />
</>
);}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog(['A0', 'B0']);
expect(container.textContent).toEqual('A0B0');
// Update b but not a
await act(() => {
store.set({a: 0, b: 1});
});// Only b re-renders
assertLog(['B1']);
expect(container.textContent).toEqual('A0B1');
// Update a but not b
await act(() => {
store.set({a: 1, b: 1});
});// Only a re-renders
assertLog(['A1']);
expect(container.textContent).toEqual('A1B1');
});// In React 18, you can't observe in between a sync render and its
// passive effects, so this is only relevant to legacy roots
// @gate enableUseSyncExternalStoreShim
test(
"compares to current state before bailing out, even when there's a " +
'mutation in between the sync and passive effects',
async () => {
const store = createExternalStore(0);
function App() {
const value = useSyncExternalStore(store.subscribe, store.getState);
useEffect(() => {
Scheduler.log('Passive effect: ' + value);
}, [value]);
return <Text text={value} />;
}const container = document.createElement('div');
const root = createRoot(container);
await act(() => root.render(<App />));
assertLog([0, 'Passive effect: 0']);
// Schedule an update. We'll intentionally not use `act` so that we can
// insert a mutation before React subscribes to the store in a
// passive effect.
store.set(1);
assertLog([
1,
// Passive effect hasn't fired yet
]);expect(container.textContent).toEqual('1');
// Flip the store state back to the previous value.
store.set(0);
assertLog([
'Passive effect: 1',
// Re-render. If the current state were tracked by updating a ref in a
// passive effect, then this would break because the previous render's
// passive effect hasn't fired yet, so we'd incorrectly think that
// the state hasn't changed.
0,
]);// Should flip back to 0
expect(container.textContent).toEqual('0');
},);test('mutating the store in between render and commit when getSnapshot has changed', async () => {
const store = createExternalStore({a: 1, b: 1});
const getSnapshotA = () => store.getState().a;
const getSnapshotB = () => store.getState().b;
function Child1({step}) {
const value = useSyncExternalStore(store.subscribe, store.getState);
useLayoutEffect(() => {
if (step === 1) {
// Update B in a layout effect. This happens in the same commit
// that changed the getSnapshot in Child2. Child2's effects haven't
// fired yet, so it doesn't have access to the latest getSnapshot. So
// it can't use the getSnapshot to bail out.
Scheduler.log('Update B in commit phase');
store.set({a: value.a, b: 2});
}}, [step]);
return null;
}function Child2({step}) {
const label = step === 0 ? 'A' : 'B';
const getSnapshot = step === 0 ? getSnapshotA : getSnapshotB;
const value = useSyncExternalStore(store.subscribe, getSnapshot);
return <Text text={label + value} />;
}let setStep;
function App() {
const [step, _setStep] = useState(0);
setStep = _setStep;
return (
<><Child1 step={step} />
<Child2 step={step} />
</>
);}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog(['A1']);
expect(container.textContent).toEqual('A1');
await act(() => {// Change getSnapshot and update the store in the same batchsetStep(1);});assertLog([
'B1','Update B in commit phase',// If Child2 had used the old getSnapshot to bail out, then it would have// incorrectly bailed out here instead of re-rendering.'B2',]);
expect(container.textContent).toEqual('B2');
});test('mutating the store in between render and commit when getSnapshot has _not_ changed', async () => {// Same as previous test, but `getSnapshot` does not changeconst store = createExternalStore({a: 1, b: 1});const getSnapshotA = () => store.getState().a;
function Child1({step}) {const value = useSyncExternalStore(store.subscribe, store.getState);
useLayoutEffect(() => {if (step === 1) {// Update B in a layout effect. This happens in the same commit
// that changed the getSnapshot in Child2. Child2's effects haven't
// fired yet, so it doesn't have access to the latest getSnapshot. So
// it can't use the getSnapshot to bail out.
Scheduler.log('Update B in commit phase');
store.set({a: value.a, b: 2});
}}, [step]);
return null;}function Child2({step}) {const value = useSyncExternalStore(store.subscribe, getSnapshotA);
return <Text text={'A' + value} />;
}let setStep;function App() {const [step, _setStep] = useState(0);
setStep = _setStep;return (<><Child1 step={step} /><Child2 step={step} /></>);}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog(['A1']);
expect(container.textContent).toEqual('A1');
// This will cause a layout effect, and in the layout effect we'll update// the storeawait act(() => {setStep(1);});assertLog([
'A1',// This updates B, but since Child2 doesn't subscribe to B, it doesn't// need to re-render.'Update B in commit phase',// No re-render]);
expect(container.textContent).toEqual('A1');
});test("does not bail out if the previous update hasn't finished yet", async () => {const store = createExternalStore(0);function Child1() {const value = useSyncExternalStore(store.subscribe, store.getState);
useLayoutEffect(() => {if (value === 1) {Scheduler.log('Reset back to 0');
store.set(0);
}}, [value]);
return <Text text={value} />;}function Child2() {const value = useSyncExternalStore(store.subscribe, store.getState);
return <Text text={value} />;}const container = document.createElement('div');
const root = createRoot(container);await act(() =>root.render(
<><Child1 /><Child2 /></>,),);assertLog([0, 0]);
expect(container.textContent).toEqual('00');
await act(() => {store.set(1);
});assertLog([1, 1, 'Reset back to 0', 0, 0]);
expect(container.textContent).toEqual('00');
});test('uses the latest getSnapshot, even if it changed in the same batch as a store update', async () => {const store = createExternalStore({a: 0, b: 0});const getSnapshotA = () => store.getState().a;
const getSnapshotB = () => store.getState().b;
let setGetSnapshot;function App() {const [getSnapshot, _setGetSnapshot] = useState(() => getSnapshotA);
setGetSnapshot = _setGetSnapshot;const text = useSyncExternalStore(store.subscribe, getSnapshot);
return <Text text={text} />;}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog([0]);
// Update the store and getSnapshot at the same timeawait act(() => {ReactDOM.flushSync(() => {
setGetSnapshot(() => getSnapshotB);store.set({a: 1, b: 2});
});});// It should read from B instead of AassertLog([2]);
expect(container.textContent).toEqual('2');
});test('handles errors thrown by getSnapshot', async () => {class ErrorBoundary extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return <Text text={this.state.error.message} />;
}return this.props.children;
}}const store = createExternalStore({value: 0,throwInGetSnapshot: false,throwInIsEqual: false,});function App() {const {value} = useSyncExternalStore(store.subscribe, () => {
const state = store.getState();
if (state.throwInGetSnapshot) {
throw new Error('Error in getSnapshot');}return state;});return <Text text={value} />;}const errorBoundary = React.createRef(null);
const container = document.createElement('div');
const root = createRoot(container);await act(() =>root.render(
<ErrorBoundary ref={errorBoundary}><App /></ErrorBoundary>,),);assertLog([0]);
expect(container.textContent).toEqual('0');
// Update that throws in a getSnapshot. We can catch it with an error boundary.
await act(() => {store.set({value: 1, throwInGetSnapshot: true, throwInIsEqual: false});
});if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
assertLog([
'Error in getSnapshot',// In a concurrent root, React renders a second time to attempt to// recover from the error.'Error in getSnapshot',]);
} else {assertLog(['Error in getSnapshot']);
}expect(container.textContent).toEqual('Error in getSnapshot');
});test('Infinite loop if getSnapshot keeps returning new reference', async () => {const store = createExternalStore({});function App() {const text = useSyncExternalStore(store.subscribe, () => ({}));
return <Text text={JSON.stringify(text)} />;
}const container = document.createElement('div');
const root = createRoot(container);await expect(async () => {expect(() =>ReactDOM.flushSync(async () => root.render(<App />)),
).toThrow(
'Maximum update depth exceeded. This can happen when a component repeatedly ' +
'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);}).toErrorDev(
'The result of getSnapshot should be cached to avoid an infinite loop',);});test('getSnapshot can return NaN without infinite loop warning', async () => {const store = createExternalStore('not a number');function App() {const value = useSyncExternalStore(store.subscribe, () =>
parseInt(store.getState(), 10),
);return <Text text={value} />;}const container = document.createElement('div');
const root = createRoot(container);// Initial render that reads a snapshot of NaN. This is OK because we use
// Object.is algorithm to compare values.
await act(() => root.render(<App />));
expect(container.textContent).toEqual('NaN');
// Update to real numberawait act(() => store.set(123));
expect(container.textContent).toEqual('123');
// Update back to NaNawait act(() => store.set('not a number'));
expect(container.textContent).toEqual('NaN');
});describe('extra features implemented in user-space', () => {// The selector implementation uses the lazy ref initialization pattern// @gate !(enableUseRefAccessWarning && __DEV__)test('memoized selectors are only called once per update', async () => {const store = createExternalStore({a: 0, b: 0});function selector(state) {Scheduler.log('Selector');
return state.a;
}function App() {Scheduler.log('App');
const a = useSyncExternalStoreWithSelector(store.subscribe,
store.getState,
null,selector,);return <Text text={'A' + a} />;
}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog(['App', 'Selector', 'A0']);
expect(container.textContent).toEqual('A0');
// Update the storeawait act(() => {store.set({a: 1, b: 0});
});assertLog([
// The selector runs before React starts rendering'Selector','App',// And because the selector didn't change during render, we can reuse// the previous result without running the selector again'A1',]);
expect(container.textContent).toEqual('A1');
});// The selector implementation uses the lazy ref initialization pattern// @gate !(enableUseRefAccessWarning && __DEV__)test('Using isEqual to bailout', async () => {const store = createExternalStore({a: 0, b: 0});function A() {const {a} = useSyncExternalStoreWithSelector(store.subscribe,
store.getState,
null,state => ({a: state.a}),
(state1, state2) => state1.a === state2.a,
);return <Text text={'A' + a} />;
}function B() {const {b} = useSyncExternalStoreWithSelector(store.subscribe,
store.getState,
null,state => {return {b: state.b};
},(state1, state2) => state1.b === state2.b,
);return <Text text={'B' + b} />;
}function App() {return (<><A /><B /></>);}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog(['A0', 'B0']);
expect(container.textContent).toEqual('A0B0');
// Update b but not aawait act(() => {store.set({a: 0, b: 1});
});// Only b re-rendersassertLog(['B1']);
expect(container.textContent).toEqual('A0B1');
// Update a but not bawait act(() => {store.set({a: 1, b: 1});
});// Only a re-rendersassertLog(['A1']);
expect(container.textContent).toEqual('A1B1');
});test('basic server hydration', async () => {const store = createExternalStore('client');const ref = React.createRef();
function App() {const text = useSyncExternalStore(store.subscribe,
store.getState,
() => 'server',);useEffect(() => {Scheduler.log('Passive effect: ' + text);
}, [text]);
return (<div ref={ref}><Text text={text} /></div>);}const container = document.createElement('div');
container.innerHTML = '<div>server</div>';
const serverRenderedDiv = container.getElementsByTagName('div')[0];
if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
await act(() => {ReactDOMClient.hydrateRoot(container, <App />);
});assertLog([
// First it hydrates the server rendered HTML'server','Passive effect: server',// Then in a second paint, it re-renders with the client state'client','Passive effect: client',]);
} else {// In the userspace shim, there's no mechanism to detect whether we're// currently hydrating, so `getServerSnapshot` is not called on the// client. To avoid this server mismatch warning, user must account for
// this themselves and return the correct value inside `getSnapshot`.
await act(() => {expect(() => ReactDOM.hydrate(<App />, container)).toErrorDev(
'Text content did not match',);});assertLog(['client', 'Passive effect: client']);
}expect(container.textContent).toEqual('client');
expect(ref.current).toEqual(serverRenderedDiv);
});});test('regression test for #23150', async () => {const store = createExternalStore('Initial');function App() {const text = useSyncExternalStore(store.subscribe, store.getState);
const [derivedText, setDerivedText] = useState(text);
useEffect(() => {}, []);
if (derivedText !== text.toUpperCase()) {
setDerivedText(text.toUpperCase());
}return <Text text={derivedText} />;}const container = document.createElement('div');
const root = createRoot(container);await act(() => root.render(<App />));
assertLog(['INITIAL']);
expect(container.textContent).toEqual('INITIAL');
await act(() => {store.set('Updated');
});assertLog(['UPDATED']);
expect(container.textContent).toEqual('UPDATED');
});// The selector implementation uses the lazy ref initialization pattern
// @gate !(enableUseRefAccessWarning && __DEV__)
test('compares selection to rendered selection even if selector changes', async () => {
const store = createExternalStore({items: ['A', 'B']});
const shallowEqualArray = (a, b) => {
if (a.length !== b.length) {
return false;
}for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}}return true;
};const List = React.memo(({items}) => {
return (
<ul>
{items.map(text => (
<li key={text}>
<Text key={text} text={text} />
</li>
))}</ul>
);
});function App({step}) {
const inlineSelector = state => {
Scheduler.log('Inline selector');
return [...state.items, 'C'];
};const items = useSyncExternalStoreWithSelector(
store.subscribe,
store.getState,
null,
inlineSelector,
shallowEqualArray,
);return (
<><List items={items} />
<Text text={'Sibling: ' + step} />
</>
);}const container = document.createElement('div');
const root = createRoot(container);await act(() => {root.render(<App step={0} />);
});assertLog(['Inline selector', 'A', 'B', 'C', 'Sibling: 0']);
await act(() => {root.render(<App step={1} />);
});assertLog([
// We had to call the selector again because it's not memoized'Inline selector',// But because the result was the same (according to isEqual) we can// bail out of rendering the memoized list. These are skipped:// 'A',// 'B',// 'C','Sibling: 1',]);
});describe('selector and isEqual error handling in extra', () => {let ErrorBoundary;beforeEach(() => {ErrorBoundary = class extends React.Component {
state = {error: null};static getDerivedStateFromError(error) {return {error};}render() {if (this.state.error) {
return <Text text={this.state.error.message} />;
}return this.props.children;
}};});it('selector can throw on update', async () => {const store = createExternalStore({a: 'a'});const selector = state => {if (typeof state.a !== 'string') {
throw new TypeError('Malformed state');}return state.a.toUpperCase();
};function App() {const a = useSyncExternalStoreWithSelector(store.subscribe,
store.getState,
null,selector,);return <Text text={a} />;}const container = document.createElement('div');
const root = createRoot(container);await act(() =>root.render(
<ErrorBoundary><App /></ErrorBoundary>,),);expect(container.textContent).toEqual('A');
await expect(async () => {await act(() => {store.set({});
});}).toWarnDev(
ReactFeatureFlags.enableUseRefAccessWarning
? ['Warning: App: Unsafe read of a mutable value during render.']
: [],
);expect(container.textContent).toEqual('Malformed state');
});it('isEqual can throw on update', async () => {const store = createExternalStore({a: 'A'});const selector = state => state.a;
const isEqual = (left, right) => {if (typeof left.a !== 'string' || typeof right.a !== 'string') {
throw new TypeError('Malformed state');}return left.a.trim() === right.a.trim();
};function App() {const a = useSyncExternalStoreWithSelector(store.subscribe,
store.getState,
null,selector,isEqual,);return <Text text={a} />;}const container = document.createElement('div');
const root = createRoot(container);await act(() =>root.render(
<ErrorBoundary><App /></ErrorBoundary>,),);expect(container.textContent).toEqual('A');
await expect(async () => {await act(() => {store.set({});
});}).toWarnDev(
ReactFeatureFlags.enableUseRefAccessWarning
? ['Warning: App: Unsafe read of a mutable value during render.']
: [],
);expect(container.textContent).toEqual('Malformed state');
});});});