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 React;
    
  14. let ReactNoop;
    
  15. let Scheduler;
    
  16. let act;
    
  17. let useLayoutEffect;
    
  18. let forwardRef;
    
  19. let useImperativeHandle;
    
  20. let useRef;
    
  21. let useState;
    
  22. let use;
    
  23. let startTransition;
    
  24. let waitFor;
    
  25. let waitForAll;
    
  26. let assertLog;
    
  27. 
    
  28. // This tests the native useSyncExternalStore implementation, not the shim.
    
  29. // Tests that apply to both the native implementation and the shim should go
    
  30. // into useSyncExternalStoreShared-test.js. The reason they are separate is
    
  31. // because at some point we may start running the shared tests against vendored
    
  32. // React DOM versions (16, 17, etc) instead of React Noop.
    
  33. describe('useSyncExternalStore', () => {
    
  34.   beforeEach(() => {
    
  35.     jest.resetModules();
    
  36. 
    
  37.     React = require('react');
    
  38.     ReactNoop = require('react-noop-renderer');
    
  39.     Scheduler = require('scheduler');
    
  40.     useLayoutEffect = React.useLayoutEffect;
    
  41.     useImperativeHandle = React.useImperativeHandle;
    
  42.     forwardRef = React.forwardRef;
    
  43.     useRef = React.useRef;
    
  44.     useState = React.useState;
    
  45.     use = React.use;
    
  46.     useSyncExternalStore = React.useSyncExternalStore;
    
  47.     startTransition = React.startTransition;
    
  48. 
    
  49.     const InternalTestUtils = require('internal-test-utils');
    
  50.     waitFor = InternalTestUtils.waitFor;
    
  51.     waitForAll = InternalTestUtils.waitForAll;
    
  52.     assertLog = InternalTestUtils.assertLog;
    
  53. 
    
  54.     act = require('internal-test-utils').act;
    
  55.   });
    
  56. 
    
  57.   function Text({text}) {
    
  58.     Scheduler.log(text);
    
  59.     return text;
    
  60.   }
    
  61. 
    
  62.   function createExternalStore(initialState) {
    
  63.     const listeners = new Set();
    
  64.     let currentState = initialState;
    
  65.     return {
    
  66.       set(text) {
    
  67.         currentState = text;
    
  68.         ReactNoop.batchedUpdates(() => {
    
  69.           listeners.forEach(listener => listener());
    
  70.         });
    
  71.       },
    
  72.       subscribe(listener) {
    
  73.         listeners.add(listener);
    
  74.         return () => listeners.delete(listener);
    
  75.       },
    
  76.       getState() {
    
  77.         return currentState;
    
  78.       },
    
  79.       getSubscriberCount() {
    
  80.         return listeners.size;
    
  81.       },
    
  82.     };
    
  83.   }
    
  84. 
    
  85.   test(
    
  86.     'detects interleaved mutations during a concurrent read before ' +
    
  87.       'layout effects fire',
    
  88.     async () => {
    
  89.       const store1 = createExternalStore(0);
    
  90.       const store2 = createExternalStore(0);
    
  91. 
    
  92.       const Child = forwardRef(({store, label}, ref) => {
    
  93.         const value = useSyncExternalStore(store.subscribe, store.getState);
    
  94.         useImperativeHandle(
    
  95.           ref,
    
  96.           () => {
    
  97.             return value;
    
  98.           },
    
  99.           [],
    
  100.         );
    
  101.         return <Text text={label + value} />;
    
  102.       });
    
  103. 
    
  104.       function App({store}) {
    
  105.         const refA = useRef(null);
    
  106.         const refB = useRef(null);
    
  107.         const refC = useRef(null);
    
  108.         useLayoutEffect(() => {
    
  109.           // This layout effect reads children that depend on an external store.
    
  110.           // This demostrates whether the children are consistent when the
    
  111.           // layout phase runs.
    
  112.           const aText = refA.current;
    
  113.           const bText = refB.current;
    
  114.           const cText = refC.current;
    
  115.           Scheduler.log(
    
  116.             `Children observed during layout: A${aText}B${bText}C${cText}`,
    
  117.           );
    
  118.         });
    
  119.         return (
    
  120.           <>
    
  121.             <Child store={store} ref={refA} label="A" />
    
  122.             <Child store={store} ref={refB} label="B" />
    
  123.             <Child store={store} ref={refC} label="C" />
    
  124.           </>
    
  125.         );
    
  126.       }
    
  127. 
    
  128.       const root = ReactNoop.createRoot();
    
  129.       await act(async () => {
    
  130.         // Start a concurrent render that reads from the store, then yield.
    
  131.         startTransition(() => {
    
  132.           root.render(<App store={store1} />);
    
  133.         });
    
  134. 
    
  135.         await waitFor(['A0', 'B0']);
    
  136. 
    
  137.         // During an interleaved event, the store is mutated.
    
  138.         store1.set(1);
    
  139. 
    
  140.         // Then we continue rendering.
    
  141.         await waitForAll([
    
  142.           // C reads a newer value from the store than A or B, which means they
    
  143.           // are inconsistent.
    
  144.           'C1',
    
  145. 
    
  146.           // Before committing the layout effects, React detects that the store
    
  147.           // has been mutated. So it throws out the entire completed tree and
    
  148.           // re-renders the new values.
    
  149.           'A1',
    
  150.           'B1',
    
  151.           'C1',
    
  152.           // The layout effects reads consistent children.
    
  153.           'Children observed during layout: A1B1C1',
    
  154.         ]);
    
  155.       });
    
  156. 
    
  157.       // Now we're going test the same thing during an update that
    
  158.       // switches stores.
    
  159.       await act(async () => {
    
  160.         startTransition(() => {
    
  161.           root.render(<App store={store2} />);
    
  162.         });
    
  163. 
    
  164.         // Start a concurrent render that reads from the store, then yield.
    
  165.         await waitFor(['A0', 'B0']);
    
  166. 
    
  167.         // During an interleaved event, the store is mutated.
    
  168.         store2.set(1);
    
  169. 
    
  170.         // Then we continue rendering.
    
  171.         await waitForAll([
    
  172.           // C reads a newer value from the store than A or B, which means they
    
  173.           // are inconsistent.
    
  174.           'C1',
    
  175. 
    
  176.           // Before committing the layout effects, React detects that the store
    
  177.           // has been mutated. So it throws out the entire completed tree and
    
  178.           // re-renders the new values.
    
  179.           'A1',
    
  180.           'B1',
    
  181.           'C1',
    
  182.           // The layout effects reads consistent children.
    
  183.           'Children observed during layout: A1B1C1',
    
  184.         ]);
    
  185.       });
    
  186.     },
    
  187.   );
    
  188. 
    
  189.   test('next value is correctly cached when state is dispatched in render phase', async () => {
    
  190.     const store = createExternalStore('value:initial');
    
  191. 
    
  192.     function App() {
    
  193.       const value = useSyncExternalStore(store.subscribe, store.getState);
    
  194.       const [sameValue, setSameValue] = useState(value);
    
  195.       if (value !== sameValue) setSameValue(value);
    
  196.       return <Text text={value} />;
    
  197.     }
    
  198. 
    
  199.     const root = ReactNoop.createRoot();
    
  200.     await act(() => {
    
  201.       // Start a render that reads from the store and yields value
    
  202.       root.render(<App />);
    
  203.     });
    
  204.     assertLog(['value:initial']);
    
  205. 
    
  206.     await act(() => {
    
  207.       store.set('value:changed');
    
  208.     });
    
  209.     assertLog(['value:changed']);
    
  210. 
    
  211.     // If cached value was updated, we expect a re-render
    
  212.     await act(() => {
    
  213.       store.set('value:initial');
    
  214.     });
    
  215.     assertLog(['value:initial']);
    
  216.   });
    
  217. 
    
  218.   test(
    
  219.     'regression: suspending in shell after synchronously patching ' +
    
  220.       'up store mutation',
    
  221.     async () => {
    
  222.       // Tests a case where a store is mutated during a concurrent event, then
    
  223.       // during the sync re-render, a synchronous render is triggered.
    
  224. 
    
  225.       const store = createExternalStore('Initial');
    
  226. 
    
  227.       let resolve;
    
  228.       const promise = new Promise(r => {
    
  229.         resolve = r;
    
  230.       });
    
  231. 
    
  232.       function A() {
    
  233.         const value = useSyncExternalStore(store.subscribe, store.getState);
    
  234. 
    
  235.         if (value === 'Updated') {
    
  236.           try {
    
  237.             use(promise);
    
  238.           } catch (x) {
    
  239.             Scheduler.log('Suspend A');
    
  240.             throw x;
    
  241.           }
    
  242.         }
    
  243. 
    
  244.         return <Text text={'A: ' + value} />;
    
  245.       }
    
  246. 
    
  247.       function B() {
    
  248.         const value = useSyncExternalStore(store.subscribe, store.getState);
    
  249.         return <Text text={'B: ' + value} />;
    
  250.       }
    
  251. 
    
  252.       function App() {
    
  253.         return (
    
  254.           <>
    
  255.             <span>
    
  256.               <A />
    
  257.             </span>
    
  258.             <span>
    
  259.               <B />
    
  260.             </span>
    
  261.           </>
    
  262.         );
    
  263.       }
    
  264. 
    
  265.       const root = ReactNoop.createRoot();
    
  266.       await act(async () => {
    
  267.         // A and B both read from the same store. Partially render A.
    
  268.         startTransition(() => root.render(<App />));
    
  269.         // A reads the initial value of the store.
    
  270.         await waitFor(['A: Initial']);
    
  271. 
    
  272.         // Before B renders, mutate the store.
    
  273.         store.set('Updated');
    
  274.       });
    
  275.       assertLog([
    
  276.         // B reads the updated value of the store.
    
  277.         'B: Updated',
    
  278.         // This should a synchronous re-render of A using the updated value. In
    
  279.         // this test, this causes A to suspend.
    
  280.         'Suspend A',
    
  281.       ]);
    
  282.       // Nothing has committed, because A suspended and no fallback
    
  283.       // was provided.
    
  284.       expect(root).toMatchRenderedOutput(null);
    
  285. 
    
  286.       // Resolve the data and finish rendering.
    
  287.       await act(() => resolve());
    
  288.       assertLog(['A: Updated', 'B: Updated']);
    
  289.       expect(root).toMatchRenderedOutput(
    
  290.         <>
    
  291.           <span>A: Updated</span>
    
  292.           <span>B: Updated</span>
    
  293.         </>,
    
  294.       );
    
  295.     },
    
  296.   );
    
  297. });