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.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. let React;
    
  12. let ReactNoop;
    
  13. let act;
    
  14. let useState;
    
  15. let useMemoCache;
    
  16. let MemoCacheSentinel;
    
  17. let ErrorBoundary;
    
  18. 
    
  19. describe('useMemoCache()', () => {
    
  20.   beforeEach(() => {
    
  21.     jest.resetModules();
    
  22. 
    
  23.     React = require('react');
    
  24.     ReactNoop = require('react-noop-renderer');
    
  25.     act = require('internal-test-utils').act;
    
  26.     useState = React.useState;
    
  27.     useMemoCache = React.unstable_useMemoCache;
    
  28.     MemoCacheSentinel = Symbol.for('react.memo_cache_sentinel');
    
  29. 
    
  30.     class _ErrorBoundary extends React.Component {
    
  31.       constructor(props) {
    
  32.         super(props);
    
  33.         this.state = {hasError: false};
    
  34.       }
    
  35. 
    
  36.       static getDerivedStateFromError(error) {
    
  37.         // Update state so the next render will show the fallback UI.
    
  38.         return {hasError: true};
    
  39.       }
    
  40. 
    
  41.       componentDidCatch(error, errorInfo) {}
    
  42. 
    
  43.       render() {
    
  44.         if (this.state.hasError) {
    
  45.           // You can render any custom fallback UI
    
  46.           return <h1>Something went wrong.</h1>;
    
  47.         }
    
  48. 
    
  49.         return this.props.children;
    
  50.       }
    
  51.     }
    
  52.     ErrorBoundary = _ErrorBoundary;
    
  53.   });
    
  54. 
    
  55.   // @gate enableUseMemoCacheHook
    
  56.   test('render component using cache', async () => {
    
  57.     function Component(props) {
    
  58.       const cache = useMemoCache(1);
    
  59.       expect(Array.isArray(cache)).toBe(true);
    
  60.       expect(cache.length).toBe(1);
    
  61.       expect(cache[0]).toBe(MemoCacheSentinel);
    
  62. 
    
  63.       return 'Ok';
    
  64.     }
    
  65.     const root = ReactNoop.createRoot();
    
  66.     await act(() => {
    
  67.       root.render(<Component />);
    
  68.     });
    
  69.     expect(root).toMatchRenderedOutput('Ok');
    
  70.   });
    
  71. 
    
  72.   // @gate enableUseMemoCacheHook
    
  73.   test('update component using cache', async () => {
    
  74.     let setX;
    
  75.     let forceUpdate;
    
  76.     function Component(props) {
    
  77.       const cache = useMemoCache(5);
    
  78. 
    
  79.       // x is used to produce a `data` object passed to the child
    
  80.       const [x, _setX] = useState(0);
    
  81.       setX = _setX;
    
  82. 
    
  83.       // n is passed as-is to the child as a cache breaker
    
  84.       const [n, setN] = useState(0);
    
  85.       forceUpdate = () => setN(a => a + 1);
    
  86. 
    
  87.       const c_0 = x !== cache[0];
    
  88.       let data;
    
  89.       if (c_0) {
    
  90.         data = {text: `Count ${x}`};
    
  91.         cache[0] = x;
    
  92.         cache[1] = data;
    
  93.       } else {
    
  94.         data = cache[1];
    
  95.       }
    
  96.       const c_2 = x !== cache[2];
    
  97.       const c_3 = n !== cache[3];
    
  98.       let t0;
    
  99.       if (c_2 || c_3) {
    
  100.         t0 = <Text data={data} n={n} />;
    
  101.         cache[2] = x;
    
  102.         cache[3] = n;
    
  103.         cache[4] = t0;
    
  104.       } else {
    
  105.         t0 = cache[4];
    
  106.       }
    
  107.       return t0;
    
  108.     }
    
  109.     let data;
    
  110.     const Text = jest.fn(function Text(props) {
    
  111.       data = props.data;
    
  112.       return data.text;
    
  113.     });
    
  114. 
    
  115.     const root = ReactNoop.createRoot();
    
  116.     await act(() => {
    
  117.       root.render(<Component />);
    
  118.     });
    
  119.     expect(root).toMatchRenderedOutput('Count 0');
    
  120.     expect(Text).toBeCalledTimes(1);
    
  121.     const data0 = data;
    
  122. 
    
  123.     // Changing x should reset the data object
    
  124.     await act(() => {
    
  125.       setX(1);
    
  126.     });
    
  127.     expect(root).toMatchRenderedOutput('Count 1');
    
  128.     expect(Text).toBeCalledTimes(2);
    
  129.     expect(data).not.toBe(data0);
    
  130.     const data1 = data;
    
  131. 
    
  132.     // Forcing an unrelated update shouldn't recreate the
    
  133.     // data object.
    
  134.     await act(() => {
    
  135.       forceUpdate();
    
  136.     });
    
  137.     expect(root).toMatchRenderedOutput('Count 1');
    
  138.     expect(Text).toBeCalledTimes(3);
    
  139.     expect(data).toBe(data1); // confirm that the cache persisted across renders
    
  140.   });
    
  141. 
    
  142.   // @gate enableUseMemoCacheHook
    
  143.   test('update component using cache with setstate during render', async () => {
    
  144.     let setN;
    
  145.     function Component(props) {
    
  146.       const cache = useMemoCache(5);
    
  147. 
    
  148.       // x is used to produce a `data` object passed to the child
    
  149.       const [x] = useState(0);
    
  150. 
    
  151.       const c_0 = x !== cache[0];
    
  152.       let data;
    
  153.       if (c_0) {
    
  154.         data = {text: `Count ${x}`};
    
  155.         cache[0] = x;
    
  156.         cache[1] = data;
    
  157.       } else {
    
  158.         data = cache[1];
    
  159.       }
    
  160. 
    
  161.       // n is passed as-is to the child as a cache breaker
    
  162.       const [n, _setN] = useState(0);
    
  163.       setN = _setN;
    
  164. 
    
  165.       if (n === 1) {
    
  166.         setN(2);
    
  167.         return;
    
  168.       }
    
  169. 
    
  170.       const c_2 = x !== cache[2];
    
  171.       const c_3 = n !== cache[3];
    
  172.       let t0;
    
  173.       if (c_2 || c_3) {
    
  174.         t0 = <Text data={data} n={n} />;
    
  175.         cache[2] = x;
    
  176.         cache[3] = n;
    
  177.         cache[4] = t0;
    
  178.       } else {
    
  179.         t0 = cache[4];
    
  180.       }
    
  181.       return t0;
    
  182.     }
    
  183.     let data;
    
  184.     const Text = jest.fn(function Text(props) {
    
  185.       data = props.data;
    
  186.       return `${data.text} (n=${props.n})`;
    
  187.     });
    
  188. 
    
  189.     const root = ReactNoop.createRoot();
    
  190.     await act(() => {
    
  191.       root.render(<Component />);
    
  192.     });
    
  193.     expect(root).toMatchRenderedOutput('Count 0 (n=0)');
    
  194.     expect(Text).toBeCalledTimes(1);
    
  195.     const data0 = data;
    
  196. 
    
  197.     // Trigger an update that will cause a setState during render. The `data` prop
    
  198.     // does not depend on `n`, and should remain cached.
    
  199.     await act(() => {
    
  200.       setN(1);
    
  201.     });
    
  202.     expect(root).toMatchRenderedOutput('Count 0 (n=2)');
    
  203.     expect(Text).toBeCalledTimes(2);
    
  204.     expect(data).toBe(data0);
    
  205.   });
    
  206. 
    
  207.   // @gate enableUseMemoCacheHook
    
  208.   test('update component using cache with throw during render', async () => {
    
  209.     let setN;
    
  210.     let shouldFail = true;
    
  211.     function Component(props) {
    
  212.       const cache = useMemoCache(5);
    
  213. 
    
  214.       // x is used to produce a `data` object passed to the child
    
  215.       const [x] = useState(0);
    
  216. 
    
  217.       const c_0 = x !== cache[0];
    
  218.       let data;
    
  219.       if (c_0) {
    
  220.         data = {text: `Count ${x}`};
    
  221.         cache[0] = x;
    
  222.         cache[1] = data;
    
  223.       } else {
    
  224.         data = cache[1];
    
  225.       }
    
  226. 
    
  227.       // n is passed as-is to the child as a cache breaker
    
  228.       const [n, _setN] = useState(0);
    
  229.       setN = _setN;
    
  230. 
    
  231.       if (n === 1) {
    
  232.         if (shouldFail) {
    
  233.           shouldFail = false;
    
  234.           throw new Error('failed');
    
  235.         }
    
  236.       }
    
  237. 
    
  238.       const c_2 = x !== cache[2];
    
  239.       const c_3 = n !== cache[3];
    
  240.       let t0;
    
  241.       if (c_2 || c_3) {
    
  242.         t0 = <Text data={data} n={n} />;
    
  243.         cache[2] = x;
    
  244.         cache[3] = n;
    
  245.         cache[4] = t0;
    
  246.       } else {
    
  247.         t0 = cache[4];
    
  248.       }
    
  249.       return t0;
    
  250.     }
    
  251.     let data;
    
  252.     const Text = jest.fn(function Text(props) {
    
  253.       data = props.data;
    
  254.       return `${data.text} (n=${props.n})`;
    
  255.     });
    
  256. 
    
  257.     spyOnDev(console, 'error');
    
  258. 
    
  259.     const root = ReactNoop.createRoot();
    
  260.     await act(() => {
    
  261.       root.render(
    
  262.         <ErrorBoundary>
    
  263.           <Component />
    
  264.         </ErrorBoundary>,
    
  265.       );
    
  266.     });
    
  267.     expect(root).toMatchRenderedOutput('Count 0 (n=0)');
    
  268.     expect(Text).toBeCalledTimes(1);
    
  269.     const data0 = data;
    
  270. 
    
  271.     await act(() => {
    
  272.       // this triggers a throw.
    
  273.       setN(1);
    
  274.     });
    
  275.     expect(root).toMatchRenderedOutput('Count 0 (n=1)');
    
  276.     expect(Text).toBeCalledTimes(2);
    
  277.     expect(data).toBe(data0);
    
  278.     const data1 = data;
    
  279. 
    
  280.     // Forcing an unrelated update shouldn't recreate the
    
  281.     // data object.
    
  282.     await act(() => {
    
  283.       setN(2);
    
  284.     });
    
  285.     expect(root).toMatchRenderedOutput('Count 0 (n=2)');
    
  286.     expect(Text).toBeCalledTimes(3);
    
  287.     expect(data).toBe(data1); // confirm that the cache persisted across renders
    
  288.   });
    
  289. 
    
  290.   // @gate enableUseMemoCacheHook
    
  291.   test('update component and custom hook with caches', async () => {
    
  292.     let setX;
    
  293.     let forceUpdate;
    
  294.     function Component(props) {
    
  295.       const cache = useMemoCache(4);
    
  296. 
    
  297.       // x is used to produce a `data` object passed to the child
    
  298.       const [x, _setX] = useState(0);
    
  299.       setX = _setX;
    
  300.       const c_x = x !== cache[0];
    
  301.       cache[0] = x;
    
  302. 
    
  303.       // n is passed as-is to the child as a cache breaker
    
  304.       const [n, setN] = useState(0);
    
  305.       forceUpdate = () => setN(a => a + 1);
    
  306.       const c_n = n !== cache[1];
    
  307.       cache[1] = n;
    
  308. 
    
  309.       let _data;
    
  310.       if (c_x) {
    
  311.         _data = cache[2] = {text: `Count ${x}`};
    
  312.       } else {
    
  313.         _data = cache[2];
    
  314.       }
    
  315.       const data = useData(_data);
    
  316.       if (c_x || c_n) {
    
  317.         return (cache[3] = <Text data={data} n={n} />);
    
  318.       } else {
    
  319.         return cache[3];
    
  320.       }
    
  321.     }
    
  322.     function useData(data) {
    
  323.       const cache = useMemoCache(2);
    
  324.       const c_data = data !== cache[0];
    
  325.       cache[0] = data;
    
  326.       let nextData;
    
  327.       if (c_data) {
    
  328.         nextData = cache[1] = {text: data.text.toLowerCase()};
    
  329.       } else {
    
  330.         nextData = cache[1];
    
  331.       }
    
  332.       return nextData;
    
  333.     }
    
  334.     let data;
    
  335.     const Text = jest.fn(function Text(props) {
    
  336.       data = props.data;
    
  337.       return data.text;
    
  338.     });
    
  339. 
    
  340.     const root = ReactNoop.createRoot();
    
  341.     await act(() => {
    
  342.       root.render(<Component />);
    
  343.     });
    
  344.     expect(root).toMatchRenderedOutput('count 0');
    
  345.     expect(Text).toBeCalledTimes(1);
    
  346.     const data0 = data;
    
  347. 
    
  348.     // Changing x should reset the data object
    
  349.     await act(() => {
    
  350.       setX(1);
    
  351.     });
    
  352.     expect(root).toMatchRenderedOutput('count 1');
    
  353.     expect(Text).toBeCalledTimes(2);
    
  354.     expect(data).not.toBe(data0);
    
  355.     const data1 = data;
    
  356. 
    
  357.     // Forcing an unrelated update shouldn't recreate the
    
  358.     // data object.
    
  359.     await act(() => {
    
  360.       forceUpdate();
    
  361.     });
    
  362.     expect(root).toMatchRenderedOutput('count 1');
    
  363.     expect(Text).toBeCalledTimes(3);
    
  364.     expect(data).toBe(data1); // confirm that the cache persisted across renders
    
  365.   });
    
  366. });