1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let useSyncExternalStore;
    
  13. let useSyncExternalStoreWithSelector;
    
  14. let React;
    
  15. let ReactDOM;
    
  16. let ReactDOMClient;
    
  17. let ReactFeatureFlags;
    
  18. let Scheduler;
    
  19. let act;
    
  20. let useState;
    
  21. let useEffect;
    
  22. let useLayoutEffect;
    
  23. let assertLog;
    
  24. 
    
  25. // This tests shared behavior between the built-in and shim implementations of
    
  26. // of useSyncExternalStore.
    
  27. describe('Shared useSyncExternalStore behavior (shim and built-in)', () => {
    
  28.   beforeEach(() => {
    
  29.     jest.resetModules();
    
  30. 
    
  31.     if (gate(flags => flags.enableUseSyncExternalStoreShim)) {
    
  32.       // Remove useSyncExternalStore from the React imports so that we use the
    
  33.       // shim instead. Also removing startTransition, since we use that to
    
  34.       // detect outdated 18 alphas that don't yet include useSyncExternalStore.
    
  35.       //
    
  36.       // Longer term, we'll probably test this branch using an actual build
    
  37.       // of React 17.
    
  38.       jest.mock('react', () => {
    
  39.         const {
    
  40.           // eslint-disable-next-line no-unused-vars
    
  41.           startTransition: _,
    
  42.           // eslint-disable-next-line no-unused-vars
    
  43.           useSyncExternalStore: __,
    
  44.           ...otherExports
    
  45.         } = jest.requireActual('react');
    
  46.         return otherExports;
    
  47.       });
    
  48.     }
    
  49. 
    
  50.     React = require('react');
    
  51.     ReactDOM = require('react-dom');
    
  52.     ReactDOMClient = require('react-dom/client');
    
  53.     ReactFeatureFlags = require('shared/ReactFeatureFlags');
    
  54.     Scheduler = require('scheduler');
    
  55.     useState = React.useState;
    
  56.     useEffect = React.useEffect;
    
  57.     useLayoutEffect = React.useLayoutEffect;
    
  58. 
    
  59.     const InternalTestUtils = require('internal-test-utils');
    
  60.     assertLog = InternalTestUtils.assertLog;
    
  61. 
    
  62.     const internalAct = require('internal-test-utils').act;
    
  63. 
    
  64.     // The internal act implementation doesn't batch updates by default, since
    
  65.     // it's mostly used to test concurrent mode. But since these tests run
    
  66.     // in both concurrent and legacy mode, I'm adding batching here.
    
  67.     act = cb => internalAct(() => ReactDOM.unstable_batchedUpdates(cb));
    
  68. 
    
  69.     if (gate(flags => flags.source)) {
    
  70.       // The `shim/with-selector` module composes the main
    
  71.       // `use-sync-external-store` entrypoint. In the compiled artifacts, this
    
  72.       // is resolved to the `shim` implementation by our build config, but when
    
  73.       // running the tests against the source files, we need to tell Jest how to
    
  74.       // resolve it. Because this is a source module, this mock has no affect on
    
  75.       // the build tests.
    
  76.       jest.mock('use-sync-external-store/src/useSyncExternalStore', () =>
    
  77.         jest.requireActual('use-sync-external-store/shim'),
    
  78.       );
    
  79.     }
    
  80.     useSyncExternalStore =
    
  81.       require('use-sync-external-store/shim').useSyncExternalStore;
    
  82.     useSyncExternalStoreWithSelector =
    
  83.       require('use-sync-external-store/shim/with-selector').useSyncExternalStoreWithSelector;
    
  84.   });
    
  85. 
    
  86.   function Text({text}) {
    
  87.     Scheduler.log(text);
    
  88.     return text;
    
  89.   }
    
  90. 
    
  91.   function createRoot(container) {
    
  92.     // This wrapper function exists so we can test both legacy roots and
    
  93.     // concurrent roots.
    
  94.     if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
    
  95.       // The native implementation only exists in 18+, so we test using
    
  96.       // concurrent mode. To test the legacy root behavior in the native
    
  97.       // implementation (which is supported in the sense that it needs to have
    
  98.       // the correct behavior, despite the fact that the legacy root API
    
  99.       // triggers a warning in 18), write a test that uses
    
  100.       // createLegacyRoot directly.
    
  101.       return ReactDOMClient.createRoot(container);
    
  102.     } else {
    
  103.       ReactDOM.render(null, container);
    
  104.       return {
    
  105.         render(children) {
    
  106.           ReactDOM.render(children, container);
    
  107.         },
    
  108.       };
    
  109.     }
    
  110.   }
    
  111. 
    
  112.   function createExternalStore(initialState) {
    
  113.     const listeners = new Set();
    
  114.     let currentState = initialState;
    
  115.     return {
    
  116.       set(text) {
    
  117.         currentState = text;
    
  118.         ReactDOM.unstable_batchedUpdates(() => {
    
  119.           listeners.forEach(listener => listener());
    
  120.         });
    
  121.       },
    
  122.       subscribe(listener) {
    
  123.         listeners.add(listener);
    
  124.         return () => listeners.delete(listener);
    
  125.       },
    
  126.       getState() {
    
  127.         return currentState;
    
  128.       },
    
  129.       getSubscriberCount() {
    
  130.         return listeners.size;
    
  131.       },
    
  132.     };
    
  133.   }
    
  134. 
    
  135.   test('basic usage', async () => {
    
  136.     const store = createExternalStore('Initial');
    
  137. 
    
  138.     function App() {
    
  139.       const text = useSyncExternalStore(store.subscribe, store.getState);
    
  140.       return <Text text={text} />;
    
  141.     }
    
  142. 
    
  143.     const container = document.createElement('div');
    
  144.     const root = createRoot(container);
    
  145.     await act(() => root.render(<App />));
    
  146. 
    
  147.     assertLog(['Initial']);
    
  148.     expect(container.textContent).toEqual('Initial');
    
  149. 
    
  150.     await act(() => {
    
  151.       store.set('Updated');
    
  152.     });
    
  153.     assertLog(['Updated']);
    
  154.     expect(container.textContent).toEqual('Updated');
    
  155.   });
    
  156. 
    
  157.   test('skips re-rendering if nothing changes', async () => {
    
  158.     const store = createExternalStore('Initial');
    
  159. 
    
  160.     function App() {
    
  161.       const text = useSyncExternalStore(store.subscribe, store.getState);
    
  162.       return <Text text={text} />;
    
  163.     }
    
  164. 
    
  165.     const container = document.createElement('div');
    
  166.     const root = createRoot(container);
    
  167.     await act(() => root.render(<App />));
    
  168. 
    
  169.     assertLog(['Initial']);
    
  170.     expect(container.textContent).toEqual('Initial');
    
  171. 
    
  172.     // Update to the same value
    
  173.     await act(() => {
    
  174.       store.set('Initial');
    
  175.     });
    
  176.     // Should not re-render
    
  177.     assertLog([]);
    
  178.     expect(container.textContent).toEqual('Initial');
    
  179.   });
    
  180. 
    
  181.   test('switch to a different store', async () => {
    
  182.     const storeA = createExternalStore(0);
    
  183.     const storeB = createExternalStore(0);
    
  184. 
    
  185.     let setStore;
    
  186.     function App() {
    
  187.       const [store, _setStore] = useState(storeA);
    
  188.       setStore = _setStore;
    
  189.       const value = useSyncExternalStore(store.subscribe, store.getState);
    
  190.       return <Text text={value} />;
    
  191.     }
    
  192. 
    
  193.     const container = document.createElement('div');
    
  194.     const root = createRoot(container);
    
  195.     await act(() => root.render(<App />));
    
  196. 
    
  197.     assertLog([0]);
    
  198.     expect(container.textContent).toEqual('0');
    
  199. 
    
  200.     await act(() => {
    
  201.       storeA.set(1);
    
  202.     });
    
  203.     assertLog([1]);
    
  204.     expect(container.textContent).toEqual('1');
    
  205. 
    
  206.     // Switch stores and update in the same batch
    
  207.     await act(() => {
    
  208.       ReactDOM.flushSync(() => {
    
  209.         // This update will be disregarded
    
  210.         storeA.set(2);
    
  211.         setStore(storeB);
    
  212.       });
    
  213.     });
    
  214.     // Now reading from B instead of A
    
  215.     assertLog([0]);
    
  216.     expect(container.textContent).toEqual('0');
    
  217. 
    
  218.     // Update A
    
  219.     await act(() => {
    
  220.       storeA.set(3);
    
  221.     });
    
  222.     // Nothing happened, because we're no longer subscribed to A
    
  223.     assertLog([]);
    
  224.     expect(container.textContent).toEqual('0');
    
  225. 
    
  226.     // Update B
    
  227.     await act(() => {
    
  228.       storeB.set(1);
    
  229.     });
    
  230.     assertLog([1]);
    
  231.     expect(container.textContent).toEqual('1');
    
  232.   });
    
  233. 
    
  234.   test('selecting a specific value inside getSnapshot', async () => {
    
  235.     const store = createExternalStore({a: 0, b: 0});
    
  236. 
    
  237.     function A() {
    
  238.       const a = useSyncExternalStore(store.subscribe, () => store.getState().a);
    
  239.       return <Text text={'A' + a} />;
    
  240.     }
    
  241.     function B() {
    
  242.       const b = useSyncExternalStore(store.subscribe, () => store.getState().b);
    
  243.       return <Text text={'B' + b} />;
    
  244.     }
    
  245. 
    
  246.     function App() {
    
  247.       return (
    
  248.         <>
    
  249.           <A />
    
  250.           <B />
    
  251.         </>
    
  252.       );
    
  253.     }
    
  254. 
    
  255.     const container = document.createElement('div');
    
  256.     const root = createRoot(container);
    
  257.     await act(() => root.render(<App />));
    
  258. 
    
  259.     assertLog(['A0', 'B0']);
    
  260.     expect(container.textContent).toEqual('A0B0');
    
  261. 
    
  262.     // Update b but not a
    
  263.     await act(() => {
    
  264.       store.set({a: 0, b: 1});
    
  265.     });
    
  266.     // Only b re-renders
    
  267.     assertLog(['B1']);
    
  268.     expect(container.textContent).toEqual('A0B1');
    
  269. 
    
  270.     // Update a but not b
    
  271.     await act(() => {
    
  272.       store.set({a: 1, b: 1});
    
  273.     });
    
  274.     // Only a re-renders
    
  275.     assertLog(['A1']);
    
  276.     expect(container.textContent).toEqual('A1B1');
    
  277.   });
    
  278. 
    
  279.   // In React 18, you can't observe in between a sync render and its
    
  280.   // passive effects, so this is only relevant to legacy roots
    
  281.   // @gate enableUseSyncExternalStoreShim
    
  282.   test(
    
  283.     "compares to current state before bailing out, even when there's a " +
    
  284.       'mutation in between the sync and passive effects',
    
  285.     async () => {
    
  286.       const store = createExternalStore(0);
    
  287. 
    
  288.       function App() {
    
  289.         const value = useSyncExternalStore(store.subscribe, store.getState);
    
  290.         useEffect(() => {
    
  291.           Scheduler.log('Passive effect: ' + value);
    
  292.         }, [value]);
    
  293.         return <Text text={value} />;
    
  294.       }
    
  295. 
    
  296.       const container = document.createElement('div');
    
  297.       const root = createRoot(container);
    
  298.       await act(() => root.render(<App />));
    
  299.       assertLog([0, 'Passive effect: 0']);
    
  300. 
    
  301.       // Schedule an update. We'll intentionally not use `act` so that we can
    
  302.       // insert a mutation before React subscribes to the store in a
    
  303.       // passive effect.
    
  304.       store.set(1);
    
  305.       assertLog([
    
  306.         1,
    
  307.         // Passive effect hasn't fired yet
    
  308.       ]);
    
  309.       expect(container.textContent).toEqual('1');
    
  310. 
    
  311.       // Flip the store state back to the previous value.
    
  312.       store.set(0);
    
  313.       assertLog([
    
  314.         'Passive effect: 1',
    
  315.         // Re-render. If the current state were tracked by updating a ref in a
    
  316.         // passive effect, then this would break because the previous render's
    
  317.         // passive effect hasn't fired yet, so we'd incorrectly think that
    
  318.         // the state hasn't changed.
    
  319.         0,
    
  320.       ]);
    
  321.       // Should flip back to 0
    
  322.       expect(container.textContent).toEqual('0');
    
  323.     },
    
  324.   );
    
  325. 
    
  326.   test('mutating the store in between render and commit when getSnapshot has changed', async () => {
    
  327.     const store = createExternalStore({a: 1, b: 1});
    
  328. 
    
  329.     const getSnapshotA = () => store.getState().a;
    
  330.     const getSnapshotB = () => store.getState().b;
    
  331. 
    
  332.     function Child1({step}) {
    
  333.       const value = useSyncExternalStore(store.subscribe, store.getState);
    
  334.       useLayoutEffect(() => {
    
  335.         if (step === 1) {
    
  336.           // Update B in a layout effect. This happens in the same commit
    
  337.           // that changed the getSnapshot in Child2. Child2's effects haven't
    
  338.           // fired yet, so it doesn't have access to the latest getSnapshot. So
    
  339.           // it can't use the getSnapshot to bail out.
    
  340.           Scheduler.log('Update B in commit phase');
    
  341.           store.set({a: value.a, b: 2});
    
  342.         }
    
  343.       }, [step]);
    
  344.       return null;
    
  345.     }
    
  346. 
    
  347.     function Child2({step}) {
    
  348.       const label = step === 0 ? 'A' : 'B';
    
  349.       const getSnapshot = step === 0 ? getSnapshotA : getSnapshotB;
    
  350.       const value = useSyncExternalStore(store.subscribe, getSnapshot);
    
  351.       return <Text text={label + value} />;
    
  352.     }
    
  353. 
    
  354.     let setStep;
    
  355.     function App() {
    
  356.       const [step, _setStep] = useState(0);
    
  357.       setStep = _setStep;
    
  358.       return (
    
  359.         <>
    
  360.           <Child1 step={step} />
    
  361.           <Child2 step={step} />
    
  362.         </>
    
  363.       );
    
  364.     }
    
  365. 
    
  366.     const container = document.createElement('div');
    
  367.     const root = createRoot(container);
    
  368.     await act(() => root.render(<App />));
    
  369.     assertLog(['A1']);
    
  370.     expect(container.textContent).toEqual('A1');
    
  371. 
    
  372.     await act(() => {
    
  373.       // Change getSnapshot and update the store in the same batch
    
  374.       setStep(1);
    
  375.     });
    
  376.     assertLog([
    
  377.       'B1',
    
  378.       'Update B in commit phase',
    
  379.       // If Child2 had used the old getSnapshot to bail out, then it would have
    
  380.       // incorrectly bailed out here instead of re-rendering.
    
  381.       'B2',
    
  382.     ]);
    
  383.     expect(container.textContent).toEqual('B2');
    
  384.   });
    
  385. 
    
  386.   test('mutating the store in between render and commit when getSnapshot has _not_ changed', async () => {
    
  387.     // Same as previous test, but `getSnapshot` does not change
    
  388.     const store = createExternalStore({a: 1, b: 1});
    
  389. 
    
  390.     const getSnapshotA = () => store.getState().a;
    
  391. 
    
  392.     function Child1({step}) {
    
  393.       const value = useSyncExternalStore(store.subscribe, store.getState);
    
  394.       useLayoutEffect(() => {
    
  395.         if (step === 1) {
    
  396.           // Update B in a layout effect. This happens in the same commit
    
  397.           // that changed the getSnapshot in Child2. Child2's effects haven't
    
  398.           // fired yet, so it doesn't have access to the latest getSnapshot. So
    
  399.           // it can't use the getSnapshot to bail out.
    
  400.           Scheduler.log('Update B in commit phase');
    
  401.           store.set({a: value.a, b: 2});
    
  402.         }
    
  403.       }, [step]);
    
  404.       return null;
    
  405.     }
    
  406. 
    
  407.     function Child2({step}) {
    
  408.       const value = useSyncExternalStore(store.subscribe, getSnapshotA);
    
  409.       return <Text text={'A' + value} />;
    
  410.     }
    
  411. 
    
  412.     let setStep;
    
  413.     function App() {
    
  414.       const [step, _setStep] = useState(0);
    
  415.       setStep = _setStep;
    
  416.       return (
    
  417.         <>
    
  418.           <Child1 step={step} />
    
  419.           <Child2 step={step} />
    
  420.         </>
    
  421.       );
    
  422.     }
    
  423. 
    
  424.     const container = document.createElement('div');
    
  425.     const root = createRoot(container);
    
  426.     await act(() => root.render(<App />));
    
  427.     assertLog(['A1']);
    
  428.     expect(container.textContent).toEqual('A1');
    
  429. 
    
  430.     // This will cause a layout effect, and in the layout effect we'll update
    
  431.     // the store
    
  432.     await act(() => {
    
  433.       setStep(1);
    
  434.     });
    
  435.     assertLog([
    
  436.       'A1',
    
  437.       // This updates B, but since Child2 doesn't subscribe to B, it doesn't
    
  438.       // need to re-render.
    
  439.       'Update B in commit phase',
    
  440.       // No re-render
    
  441.     ]);
    
  442.     expect(container.textContent).toEqual('A1');
    
  443.   });
    
  444. 
    
  445.   test("does not bail out if the previous update hasn't finished yet", async () => {
    
  446.     const store = createExternalStore(0);
    
  447. 
    
  448.     function Child1() {
    
  449.       const value = useSyncExternalStore(store.subscribe, store.getState);
    
  450.       useLayoutEffect(() => {
    
  451.         if (value === 1) {
    
  452.           Scheduler.log('Reset back to 0');
    
  453.           store.set(0);
    
  454.         }
    
  455.       }, [value]);
    
  456.       return <Text text={value} />;
    
  457.     }
    
  458. 
    
  459.     function Child2() {
    
  460.       const value = useSyncExternalStore(store.subscribe, store.getState);
    
  461.       return <Text text={value} />;
    
  462.     }
    
  463. 
    
  464.     const container = document.createElement('div');
    
  465.     const root = createRoot(container);
    
  466.     await act(() =>
    
  467.       root.render(
    
  468.         <>
    
  469.           <Child1 />
    
  470.           <Child2 />
    
  471.         </>,
    
  472.       ),
    
  473.     );
    
  474.     assertLog([0, 0]);
    
  475.     expect(container.textContent).toEqual('00');
    
  476. 
    
  477.     await act(() => {
    
  478.       store.set(1);
    
  479.     });
    
  480.     assertLog([1, 1, 'Reset back to 0', 0, 0]);
    
  481.     expect(container.textContent).toEqual('00');
    
  482.   });
    
  483. 
    
  484.   test('uses the latest getSnapshot, even if it changed in the same batch as a store update', async () => {
    
  485.     const store = createExternalStore({a: 0, b: 0});
    
  486. 
    
  487.     const getSnapshotA = () => store.getState().a;
    
  488.     const getSnapshotB = () => store.getState().b;
    
  489. 
    
  490.     let setGetSnapshot;
    
  491.     function App() {
    
  492.       const [getSnapshot, _setGetSnapshot] = useState(() => getSnapshotA);
    
  493.       setGetSnapshot = _setGetSnapshot;
    
  494.       const text = useSyncExternalStore(store.subscribe, getSnapshot);
    
  495.       return <Text text={text} />;
    
  496.     }
    
  497. 
    
  498.     const container = document.createElement('div');
    
  499.     const root = createRoot(container);
    
  500.     await act(() => root.render(<App />));
    
  501.     assertLog([0]);
    
  502. 
    
  503.     // Update the store and getSnapshot at the same time
    
  504.     await act(() => {
    
  505.       ReactDOM.flushSync(() => {
    
  506.         setGetSnapshot(() => getSnapshotB);
    
  507.         store.set({a: 1, b: 2});
    
  508.       });
    
  509.     });
    
  510.     // It should read from B instead of A
    
  511.     assertLog([2]);
    
  512.     expect(container.textContent).toEqual('2');
    
  513.   });
    
  514. 
    
  515.   test('handles errors thrown by getSnapshot', async () => {
    
  516.     class ErrorBoundary extends React.Component {
    
  517.       state = {error: null};
    
  518.       static getDerivedStateFromError(error) {
    
  519.         return {error};
    
  520.       }
    
  521.       render() {
    
  522.         if (this.state.error) {
    
  523.           return <Text text={this.state.error.message} />;
    
  524.         }
    
  525.         return this.props.children;
    
  526.       }
    
  527.     }
    
  528. 
    
  529.     const store = createExternalStore({
    
  530.       value: 0,
    
  531.       throwInGetSnapshot: false,
    
  532.       throwInIsEqual: false,
    
  533.     });
    
  534. 
    
  535.     function App() {
    
  536.       const {value} = useSyncExternalStore(store.subscribe, () => {
    
  537.         const state = store.getState();
    
  538.         if (state.throwInGetSnapshot) {
    
  539.           throw new Error('Error in getSnapshot');
    
  540.         }
    
  541.         return state;
    
  542.       });
    
  543.       return <Text text={value} />;
    
  544.     }
    
  545. 
    
  546.     const errorBoundary = React.createRef(null);
    
  547.     const container = document.createElement('div');
    
  548.     const root = createRoot(container);
    
  549.     await act(() =>
    
  550.       root.render(
    
  551.         <ErrorBoundary ref={errorBoundary}>
    
  552.           <App />
    
  553.         </ErrorBoundary>,
    
  554.       ),
    
  555.     );
    
  556.     assertLog([0]);
    
  557.     expect(container.textContent).toEqual('0');
    
  558. 
    
  559.     // Update that throws in a getSnapshot. We can catch it with an error boundary.
    
  560.     await act(() => {
    
  561.       store.set({value: 1, throwInGetSnapshot: true, throwInIsEqual: false});
    
  562.     });
    
  563.     if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
    
  564.       assertLog([
    
  565.         'Error in getSnapshot',
    
  566.         // In a concurrent root, React renders a second time to attempt to
    
  567.         // recover from the error.
    
  568.         'Error in getSnapshot',
    
  569.       ]);
    
  570.     } else {
    
  571.       assertLog(['Error in getSnapshot']);
    
  572.     }
    
  573.     expect(container.textContent).toEqual('Error in getSnapshot');
    
  574.   });
    
  575. 
    
  576.   test('Infinite loop if getSnapshot keeps returning new reference', async () => {
    
  577.     const store = createExternalStore({});
    
  578. 
    
  579.     function App() {
    
  580.       const text = useSyncExternalStore(store.subscribe, () => ({}));
    
  581.       return <Text text={JSON.stringify(text)} />;
    
  582.     }
    
  583. 
    
  584.     const container = document.createElement('div');
    
  585.     const root = createRoot(container);
    
  586. 
    
  587.     await expect(async () => {
    
  588.       expect(() =>
    
  589.         ReactDOM.flushSync(async () => root.render(<App />)),
    
  590.       ).toThrow(
    
  591.         'Maximum update depth exceeded. This can happen when a component repeatedly ' +
    
  592.           'calls setState inside componentWillUpdate or componentDidUpdate. React limits ' +
    
  593.           'the number of nested updates to prevent infinite loops.',
    
  594.       );
    
  595.     }).toErrorDev(
    
  596.       'The result of getSnapshot should be cached to avoid an infinite loop',
    
  597.     );
    
  598.   });
    
  599. 
    
  600.   test('getSnapshot can return NaN without infinite loop warning', async () => {
    
  601.     const store = createExternalStore('not a number');
    
  602. 
    
  603.     function App() {
    
  604.       const value = useSyncExternalStore(store.subscribe, () =>
    
  605.         parseInt(store.getState(), 10),
    
  606.       );
    
  607.       return <Text text={value} />;
    
  608.     }
    
  609. 
    
  610.     const container = document.createElement('div');
    
  611.     const root = createRoot(container);
    
  612. 
    
  613.     // Initial render that reads a snapshot of NaN. This is OK because we use
    
  614.     // Object.is algorithm to compare values.
    
  615.     await act(() => root.render(<App />));
    
  616.     expect(container.textContent).toEqual('NaN');
    
  617. 
    
  618.     // Update to real number
    
  619.     await act(() => store.set(123));
    
  620.     expect(container.textContent).toEqual('123');
    
  621. 
    
  622.     // Update back to NaN
    
  623.     await act(() => store.set('not a number'));
    
  624.     expect(container.textContent).toEqual('NaN');
    
  625.   });
    
  626. 
    
  627.   describe('extra features implemented in user-space', () => {
    
  628.     // The selector implementation uses the lazy ref initialization pattern
    
  629.     // @gate !(enableUseRefAccessWarning && __DEV__)
    
  630.     test('memoized selectors are only called once per update', async () => {
    
  631.       const store = createExternalStore({a: 0, b: 0});
    
  632. 
    
  633.       function selector(state) {
    
  634.         Scheduler.log('Selector');
    
  635.         return state.a;
    
  636.       }
    
  637. 
    
  638.       function App() {
    
  639.         Scheduler.log('App');
    
  640.         const a = useSyncExternalStoreWithSelector(
    
  641.           store.subscribe,
    
  642.           store.getState,
    
  643.           null,
    
  644.           selector,
    
  645.         );
    
  646.         return <Text text={'A' + a} />;
    
  647.       }
    
  648. 
    
  649.       const container = document.createElement('div');
    
  650.       const root = createRoot(container);
    
  651.       await act(() => root.render(<App />));
    
  652. 
    
  653.       assertLog(['App', 'Selector', 'A0']);
    
  654.       expect(container.textContent).toEqual('A0');
    
  655. 
    
  656.       // Update the store
    
  657.       await act(() => {
    
  658.         store.set({a: 1, b: 0});
    
  659.       });
    
  660.       assertLog([
    
  661.         // The selector runs before React starts rendering
    
  662.         'Selector',
    
  663.         'App',
    
  664.         // And because the selector didn't change during render, we can reuse
    
  665.         // the previous result without running the selector again
    
  666.         'A1',
    
  667.       ]);
    
  668.       expect(container.textContent).toEqual('A1');
    
  669.     });
    
  670. 
    
  671.     // The selector implementation uses the lazy ref initialization pattern
    
  672.     // @gate !(enableUseRefAccessWarning && __DEV__)
    
  673.     test('Using isEqual to bailout', async () => {
    
  674.       const store = createExternalStore({a: 0, b: 0});
    
  675. 
    
  676.       function A() {
    
  677.         const {a} = useSyncExternalStoreWithSelector(
    
  678.           store.subscribe,
    
  679.           store.getState,
    
  680.           null,
    
  681.           state => ({a: state.a}),
    
  682.           (state1, state2) => state1.a === state2.a,
    
  683.         );
    
  684.         return <Text text={'A' + a} />;
    
  685.       }
    
  686.       function B() {
    
  687.         const {b} = useSyncExternalStoreWithSelector(
    
  688.           store.subscribe,
    
  689.           store.getState,
    
  690.           null,
    
  691.           state => {
    
  692.             return {b: state.b};
    
  693.           },
    
  694.           (state1, state2) => state1.b === state2.b,
    
  695.         );
    
  696.         return <Text text={'B' + b} />;
    
  697.       }
    
  698. 
    
  699.       function App() {
    
  700.         return (
    
  701.           <>
    
  702.             <A />
    
  703.             <B />
    
  704.           </>
    
  705.         );
    
  706.       }
    
  707. 
    
  708.       const container = document.createElement('div');
    
  709.       const root = createRoot(container);
    
  710.       await act(() => root.render(<App />));
    
  711. 
    
  712.       assertLog(['A0', 'B0']);
    
  713.       expect(container.textContent).toEqual('A0B0');
    
  714. 
    
  715.       // Update b but not a
    
  716.       await act(() => {
    
  717.         store.set({a: 0, b: 1});
    
  718.       });
    
  719.       // Only b re-renders
    
  720.       assertLog(['B1']);
    
  721.       expect(container.textContent).toEqual('A0B1');
    
  722. 
    
  723.       // Update a but not b
    
  724.       await act(() => {
    
  725.         store.set({a: 1, b: 1});
    
  726.       });
    
  727.       // Only a re-renders
    
  728.       assertLog(['A1']);
    
  729.       expect(container.textContent).toEqual('A1B1');
    
  730.     });
    
  731. 
    
  732.     test('basic server hydration', async () => {
    
  733.       const store = createExternalStore('client');
    
  734. 
    
  735.       const ref = React.createRef();
    
  736.       function App() {
    
  737.         const text = useSyncExternalStore(
    
  738.           store.subscribe,
    
  739.           store.getState,
    
  740.           () => 'server',
    
  741.         );
    
  742.         useEffect(() => {
    
  743.           Scheduler.log('Passive effect: ' + text);
    
  744.         }, [text]);
    
  745.         return (
    
  746.           <div ref={ref}>
    
  747.             <Text text={text} />
    
  748.           </div>
    
  749.         );
    
  750.       }
    
  751. 
    
  752.       const container = document.createElement('div');
    
  753.       container.innerHTML = '<div>server</div>';
    
  754.       const serverRenderedDiv = container.getElementsByTagName('div')[0];
    
  755. 
    
  756.       if (gate(flags => !flags.enableUseSyncExternalStoreShim)) {
    
  757.         await act(() => {
    
  758.           ReactDOMClient.hydrateRoot(container, <App />);
    
  759.         });
    
  760.         assertLog([
    
  761.           // First it hydrates the server rendered HTML
    
  762.           'server',
    
  763.           'Passive effect: server',
    
  764.           // Then in a second paint, it re-renders with the client state
    
  765.           'client',
    
  766.           'Passive effect: client',
    
  767.         ]);
    
  768.       } else {
    
  769.         // In the userspace shim, there's no mechanism to detect whether we're
    
  770.         // currently hydrating, so `getServerSnapshot` is not called on the
    
  771.         // client. To avoid this server mismatch warning, user must account for
    
  772.         // this themselves and return the correct value inside `getSnapshot`.
    
  773.         await act(() => {
    
  774.           expect(() => ReactDOM.hydrate(<App />, container)).toErrorDev(
    
  775.             'Text content did not match',
    
  776.           );
    
  777.         });
    
  778.         assertLog(['client', 'Passive effect: client']);
    
  779.       }
    
  780.       expect(container.textContent).toEqual('client');
    
  781.       expect(ref.current).toEqual(serverRenderedDiv);
    
  782.     });
    
  783.   });
    
  784. 
    
  785.   test('regression test for #23150', async () => {
    
  786.     const store = createExternalStore('Initial');
    
  787. 
    
  788.     function App() {
    
  789.       const text = useSyncExternalStore(store.subscribe, store.getState);
    
  790.       const [derivedText, setDerivedText] = useState(text);
    
  791.       useEffect(() => {}, []);
    
  792.       if (derivedText !== text.toUpperCase()) {
    
  793.         setDerivedText(text.toUpperCase());
    
  794.       }
    
  795.       return <Text text={derivedText} />;
    
  796.     }
    
  797. 
    
  798.     const container = document.createElement('div');
    
  799.     const root = createRoot(container);
    
  800.     await act(() => root.render(<App />));
    
  801. 
    
  802.     assertLog(['INITIAL']);
    
  803.     expect(container.textContent).toEqual('INITIAL');
    
  804. 
    
  805.     await act(() => {
    
  806.       store.set('Updated');
    
  807.     });
    
  808.     assertLog(['UPDATED']);
    
  809.     expect(container.textContent).toEqual('UPDATED');
    
  810.   });
    
  811. 
    
  812.   // The selector implementation uses the lazy ref initialization pattern
    
  813.   // @gate !(enableUseRefAccessWarning && __DEV__)
    
  814.   test('compares selection to rendered selection even if selector changes', async () => {
    
  815.     const store = createExternalStore({items: ['A', 'B']});
    
  816. 
    
  817.     const shallowEqualArray = (a, b) => {
    
  818.       if (a.length !== b.length) {
    
  819.         return false;
    
  820.       }
    
  821.       for (let i = 0; i < a.length; i++) {
    
  822.         if (a[i] !== b[i]) {
    
  823.           return false;
    
  824.         }
    
  825.       }
    
  826.       return true;
    
  827.     };
    
  828. 
    
  829.     const List = React.memo(({items}) => {
    
  830.       return (
    
  831.         <ul>
    
  832.           {items.map(text => (
    
  833.             <li key={text}>
    
  834.               <Text key={text} text={text} />
    
  835.             </li>
    
  836.           ))}
    
  837.         </ul>
    
  838.       );
    
  839.     });
    
  840. 
    
  841.     function App({step}) {
    
  842.       const inlineSelector = state => {
    
  843.         Scheduler.log('Inline selector');
    
  844.         return [...state.items, 'C'];
    
  845.       };
    
  846.       const items = useSyncExternalStoreWithSelector(
    
  847.         store.subscribe,
    
  848.         store.getState,
    
  849.         null,
    
  850.         inlineSelector,
    
  851.         shallowEqualArray,
    
  852.       );
    
  853.       return (
    
  854.         <>
    
  855.           <List items={items} />
    
  856.           <Text text={'Sibling: ' + step} />
    
  857.         </>
    
  858.       );
    
  859.     }
    
  860. 
    
  861.     const container = document.createElement('div');
    
  862.     const root = createRoot(container);
    
  863.     await act(() => {
    
  864.       root.render(<App step={0} />);
    
  865.     });
    
  866.     assertLog(['Inline selector', 'A', 'B', 'C', 'Sibling: 0']);
    
  867. 
    
  868.     await act(() => {
    
  869.       root.render(<App step={1} />);
    
  870.     });
    
  871.     assertLog([
    
  872.       // We had to call the selector again because it's not memoized
    
  873.       'Inline selector',
    
  874. 
    
  875.       // But because the result was the same (according to isEqual) we can
    
  876.       // bail out of rendering the memoized list. These are skipped:
    
  877.       // 'A',
    
  878.       // 'B',
    
  879.       // 'C',
    
  880. 
    
  881.       'Sibling: 1',
    
  882.     ]);
    
  883.   });
    
  884. 
    
  885.   describe('selector and isEqual error handling in extra', () => {
    
  886.     let ErrorBoundary;
    
  887.     beforeEach(() => {
    
  888.       ErrorBoundary = class extends React.Component {
    
  889.         state = {error: null};
    
  890.         static getDerivedStateFromError(error) {
    
  891.           return {error};
    
  892.         }
    
  893.         render() {
    
  894.           if (this.state.error) {
    
  895.             return <Text text={this.state.error.message} />;
    
  896.           }
    
  897.           return this.props.children;
    
  898.         }
    
  899.       };
    
  900.     });
    
  901. 
    
  902.     it('selector can throw on update', async () => {
    
  903.       const store = createExternalStore({a: 'a'});
    
  904.       const selector = state => {
    
  905.         if (typeof state.a !== 'string') {
    
  906.           throw new TypeError('Malformed state');
    
  907.         }
    
  908.         return state.a.toUpperCase();
    
  909.       };
    
  910. 
    
  911.       function App() {
    
  912.         const a = useSyncExternalStoreWithSelector(
    
  913.           store.subscribe,
    
  914.           store.getState,
    
  915.           null,
    
  916.           selector,
    
  917.         );
    
  918.         return <Text text={a} />;
    
  919.       }
    
  920. 
    
  921.       const container = document.createElement('div');
    
  922.       const root = createRoot(container);
    
  923.       await act(() =>
    
  924.         root.render(
    
  925.           <ErrorBoundary>
    
  926.             <App />
    
  927.           </ErrorBoundary>,
    
  928.         ),
    
  929.       );
    
  930. 
    
  931.       expect(container.textContent).toEqual('A');
    
  932. 
    
  933.       await expect(async () => {
    
  934.         await act(() => {
    
  935.           store.set({});
    
  936.         });
    
  937.       }).toWarnDev(
    
  938.         ReactFeatureFlags.enableUseRefAccessWarning
    
  939.           ? ['Warning: App: Unsafe read of a mutable value during render.']
    
  940.           : [],
    
  941.       );
    
  942.       expect(container.textContent).toEqual('Malformed state');
    
  943.     });
    
  944. 
    
  945.     it('isEqual can throw on update', async () => {
    
  946.       const store = createExternalStore({a: 'A'});
    
  947.       const selector = state => state.a;
    
  948.       const isEqual = (left, right) => {
    
  949.         if (typeof left.a !== 'string' || typeof right.a !== 'string') {
    
  950.           throw new TypeError('Malformed state');
    
  951.         }
    
  952.         return left.a.trim() === right.a.trim();
    
  953.       };
    
  954. 
    
  955.       function App() {
    
  956.         const a = useSyncExternalStoreWithSelector(
    
  957.           store.subscribe,
    
  958.           store.getState,
    
  959.           null,
    
  960.           selector,
    
  961.           isEqual,
    
  962.         );
    
  963.         return <Text text={a} />;
    
  964.       }
    
  965. 
    
  966.       const container = document.createElement('div');
    
  967.       const root = createRoot(container);
    
  968.       await act(() =>
    
  969.         root.render(
    
  970.           <ErrorBoundary>
    
  971.             <App />
    
  972.           </ErrorBoundary>,
    
  973.         ),
    
  974.       );
    
  975. 
    
  976.       expect(container.textContent).toEqual('A');
    
  977. 
    
  978.       await expect(async () => {
    
  979.         await act(() => {
    
  980.           store.set({});
    
  981.         });
    
  982.       }).toWarnDev(
    
  983.         ReactFeatureFlags.enableUseRefAccessWarning
    
  984.           ? ['Warning: App: Unsafe read of a mutable value during render.']
    
  985.           : [],
    
  986.       );
    
  987.       expect(container.textContent).toEqual('Malformed state');
    
  988.     });
    
  989.   });
    
  990. });