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. 
    
  8. 'use strict';
    
  9. 
    
  10. let React;
    
  11. let ReactNoop;
    
  12. let Scheduler;
    
  13. let act;
    
  14. let startTransition;
    
  15. let useDeferredValue;
    
  16. let useMemo;
    
  17. let useState;
    
  18. let Suspense;
    
  19. let Activity;
    
  20. let assertLog;
    
  21. let waitForPaint;
    
  22. let textCache;
    
  23. 
    
  24. describe('ReactDeferredValue', () => {
    
  25.   beforeEach(() => {
    
  26.     jest.resetModules();
    
  27. 
    
  28.     React = require('react');
    
  29.     ReactNoop = require('react-noop-renderer');
    
  30.     Scheduler = require('scheduler');
    
  31.     act = require('internal-test-utils').act;
    
  32.     startTransition = React.startTransition;
    
  33.     useDeferredValue = React.useDeferredValue;
    
  34.     useMemo = React.useMemo;
    
  35.     useState = React.useState;
    
  36.     Suspense = React.Suspense;
    
  37.     Activity = React.unstable_Activity;
    
  38. 
    
  39.     const InternalTestUtils = require('internal-test-utils');
    
  40.     assertLog = InternalTestUtils.assertLog;
    
  41.     waitForPaint = InternalTestUtils.waitForPaint;
    
  42. 
    
  43.     textCache = new Map();
    
  44.   });
    
  45. 
    
  46.   function resolveText(text) {
    
  47.     const record = textCache.get(text);
    
  48.     if (record === undefined) {
    
  49.       const newRecord = {
    
  50.         status: 'resolved',
    
  51.         value: text,
    
  52.       };
    
  53.       textCache.set(text, newRecord);
    
  54.     } else if (record.status === 'pending') {
    
  55.       const thenable = record.value;
    
  56.       record.status = 'resolved';
    
  57.       record.value = text;
    
  58.       thenable.pings.forEach(t => t());
    
  59.     }
    
  60.   }
    
  61. 
    
  62.   function readText(text) {
    
  63.     const record = textCache.get(text);
    
  64.     if (record !== undefined) {
    
  65.       switch (record.status) {
    
  66.         case 'pending':
    
  67.           Scheduler.log(`Suspend! [${text}]`);
    
  68.           throw record.value;
    
  69.         case 'rejected':
    
  70.           throw record.value;
    
  71.         case 'resolved':
    
  72.           return record.value;
    
  73.       }
    
  74.     } else {
    
  75.       Scheduler.log(`Suspend! [${text}]`);
    
  76.       const thenable = {
    
  77.         pings: [],
    
  78.         then(resolve) {
    
  79.           if (newRecord.status === 'pending') {
    
  80.             thenable.pings.push(resolve);
    
  81.           } else {
    
  82.             Promise.resolve().then(() => resolve(newRecord.value));
    
  83.           }
    
  84.         },
    
  85.       };
    
  86. 
    
  87.       const newRecord = {
    
  88.         status: 'pending',
    
  89.         value: thenable,
    
  90.       };
    
  91.       textCache.set(text, newRecord);
    
  92. 
    
  93.       throw thenable;
    
  94.     }
    
  95.   }
    
  96. 
    
  97.   function Text({text}) {
    
  98.     Scheduler.log(text);
    
  99.     return text;
    
  100.   }
    
  101. 
    
  102.   function AsyncText({text}) {
    
  103.     readText(text);
    
  104.     Scheduler.log(text);
    
  105.     return text;
    
  106.   }
    
  107. 
    
  108.   it('does not cause an infinite defer loop if the original value isn\t memoized', async () => {
    
  109.     function App({value}) {
    
  110.       // The object passed to useDeferredValue is never the same as the previous
    
  111.       // render. A naive implementation would endlessly spawn deferred renders.
    
  112.       const {value: deferredValue} = useDeferredValue({value});
    
  113. 
    
  114.       const child = useMemo(
    
  115.         () => <Text text={'Original: ' + value} />,
    
  116.         [value],
    
  117.       );
    
  118. 
    
  119.       const deferredChild = useMemo(
    
  120.         () => <Text text={'Deferred: ' + deferredValue} />,
    
  121.         [deferredValue],
    
  122.       );
    
  123. 
    
  124.       return (
    
  125.         <div>
    
  126.           <div>{child}</div>
    
  127.           <div>{deferredChild}</div>
    
  128.         </div>
    
  129.       );
    
  130.     }
    
  131. 
    
  132.     const root = ReactNoop.createRoot();
    
  133. 
    
  134.     // Initial render
    
  135.     await act(() => {
    
  136.       root.render(<App value={1} />);
    
  137.     });
    
  138.     assertLog(['Original: 1', 'Deferred: 1']);
    
  139. 
    
  140.     // If it's an urgent update, the value is deferred
    
  141.     await act(async () => {
    
  142.       root.render(<App value={2} />);
    
  143. 
    
  144.       await waitForPaint(['Original: 2']);
    
  145.       // The deferred value updates in a separate render
    
  146.       await waitForPaint(['Deferred: 2']);
    
  147.     });
    
  148.     expect(root).toMatchRenderedOutput(
    
  149.       <div>
    
  150.         <div>Original: 2</div>
    
  151.         <div>Deferred: 2</div>
    
  152.       </div>,
    
  153.     );
    
  154. 
    
  155.     // But if it updates during a transition, it doesn't defer
    
  156.     await act(async () => {
    
  157.       startTransition(() => {
    
  158.         root.render(<App value={3} />);
    
  159.       });
    
  160.       // The deferred value updates in the same render as the original
    
  161.       await waitForPaint(['Original: 3', 'Deferred: 3']);
    
  162.     });
    
  163.     expect(root).toMatchRenderedOutput(
    
  164.       <div>
    
  165.         <div>Original: 3</div>
    
  166.         <div>Deferred: 3</div>
    
  167.       </div>,
    
  168.     );
    
  169.   });
    
  170. 
    
  171.   it('does not defer during a transition', async () => {
    
  172.     function App({value}) {
    
  173.       const deferredValue = useDeferredValue(value);
    
  174. 
    
  175.       const child = useMemo(
    
  176.         () => <Text text={'Original: ' + value} />,
    
  177.         [value],
    
  178.       );
    
  179. 
    
  180.       const deferredChild = useMemo(
    
  181.         () => <Text text={'Deferred: ' + deferredValue} />,
    
  182.         [deferredValue],
    
  183.       );
    
  184. 
    
  185.       return (
    
  186.         <div>
    
  187.           <div>{child}</div>
    
  188.           <div>{deferredChild}</div>
    
  189.         </div>
    
  190.       );
    
  191.     }
    
  192. 
    
  193.     const root = ReactNoop.createRoot();
    
  194. 
    
  195.     // Initial render
    
  196.     await act(() => {
    
  197.       root.render(<App value={1} />);
    
  198.     });
    
  199.     assertLog(['Original: 1', 'Deferred: 1']);
    
  200. 
    
  201.     // If it's an urgent update, the value is deferred
    
  202.     await act(async () => {
    
  203.       root.render(<App value={2} />);
    
  204. 
    
  205.       await waitForPaint(['Original: 2']);
    
  206.       // The deferred value updates in a separate render
    
  207.       await waitForPaint(['Deferred: 2']);
    
  208.     });
    
  209.     expect(root).toMatchRenderedOutput(
    
  210.       <div>
    
  211.         <div>Original: 2</div>
    
  212.         <div>Deferred: 2</div>
    
  213.       </div>,
    
  214.     );
    
  215. 
    
  216.     // But if it updates during a transition, it doesn't defer
    
  217.     await act(async () => {
    
  218.       startTransition(() => {
    
  219.         root.render(<App value={3} />);
    
  220.       });
    
  221.       // The deferred value updates in the same render as the original
    
  222.       await waitForPaint(['Original: 3', 'Deferred: 3']);
    
  223.     });
    
  224.     expect(root).toMatchRenderedOutput(
    
  225.       <div>
    
  226.         <div>Original: 3</div>
    
  227.         <div>Deferred: 3</div>
    
  228.       </div>,
    
  229.     );
    
  230.   });
    
  231. 
    
  232.   it("works if there's a render phase update", async () => {
    
  233.     function App({value: propValue}) {
    
  234.       const [value, setValue] = useState(null);
    
  235.       if (value !== propValue) {
    
  236.         setValue(propValue);
    
  237.       }
    
  238. 
    
  239.       const deferredValue = useDeferredValue(value);
    
  240. 
    
  241.       const child = useMemo(
    
  242.         () => <Text text={'Original: ' + value} />,
    
  243.         [value],
    
  244.       );
    
  245. 
    
  246.       const deferredChild = useMemo(
    
  247.         () => <Text text={'Deferred: ' + deferredValue} />,
    
  248.         [deferredValue],
    
  249.       );
    
  250. 
    
  251.       return (
    
  252.         <div>
    
  253.           <div>{child}</div>
    
  254.           <div>{deferredChild}</div>
    
  255.         </div>
    
  256.       );
    
  257.     }
    
  258. 
    
  259.     const root = ReactNoop.createRoot();
    
  260. 
    
  261.     // Initial render
    
  262.     await act(() => {
    
  263.       root.render(<App value={1} />);
    
  264.     });
    
  265.     assertLog(['Original: 1', 'Deferred: 1']);
    
  266. 
    
  267.     // If it's an urgent update, the value is deferred
    
  268.     await act(async () => {
    
  269.       root.render(<App value={2} />);
    
  270. 
    
  271.       await waitForPaint(['Original: 2']);
    
  272.       // The deferred value updates in a separate render
    
  273.       await waitForPaint(['Deferred: 2']);
    
  274.     });
    
  275.     expect(root).toMatchRenderedOutput(
    
  276.       <div>
    
  277.         <div>Original: 2</div>
    
  278.         <div>Deferred: 2</div>
    
  279.       </div>,
    
  280.     );
    
  281. 
    
  282.     // But if it updates during a transition, it doesn't defer
    
  283.     await act(async () => {
    
  284.       startTransition(() => {
    
  285.         root.render(<App value={3} />);
    
  286.       });
    
  287.       // The deferred value updates in the same render as the original
    
  288.       await waitForPaint(['Original: 3', 'Deferred: 3']);
    
  289.     });
    
  290.     expect(root).toMatchRenderedOutput(
    
  291.       <div>
    
  292.         <div>Original: 3</div>
    
  293.         <div>Deferred: 3</div>
    
  294.       </div>,
    
  295.     );
    
  296.   });
    
  297. 
    
  298.   it('regression test: during urgent update, reuse previous value, not initial value', async () => {
    
  299.     function App({value: propValue}) {
    
  300.       const [value, setValue] = useState(null);
    
  301.       if (value !== propValue) {
    
  302.         setValue(propValue);
    
  303.       }
    
  304. 
    
  305.       const deferredValue = useDeferredValue(value);
    
  306. 
    
  307.       const child = useMemo(
    
  308.         () => <Text text={'Original: ' + value} />,
    
  309.         [value],
    
  310.       );
    
  311. 
    
  312.       const deferredChild = useMemo(
    
  313.         () => <Text text={'Deferred: ' + deferredValue} />,
    
  314.         [deferredValue],
    
  315.       );
    
  316. 
    
  317.       return (
    
  318.         <div>
    
  319.           <div>{child}</div>
    
  320.           <div>{deferredChild}</div>
    
  321.         </div>
    
  322.       );
    
  323.     }
    
  324. 
    
  325.     const root = ReactNoop.createRoot();
    
  326. 
    
  327.     // Initial render
    
  328.     await act(async () => {
    
  329.       root.render(<App value={1} />);
    
  330.       await waitForPaint(['Original: 1', 'Deferred: 1']);
    
  331.       expect(root).toMatchRenderedOutput(
    
  332.         <div>
    
  333.           <div>Original: 1</div>
    
  334.           <div>Deferred: 1</div>
    
  335.         </div>,
    
  336.       );
    
  337.     });
    
  338. 
    
  339.     await act(async () => {
    
  340.       startTransition(() => {
    
  341.         root.render(<App value={2} />);
    
  342.       });
    
  343.       // In the regression, the memoized value was not updated during non-urgent
    
  344.       // updates, so this would flip the deferred value back to the initial
    
  345.       // value (1) instead of reusing the current one (2).
    
  346.       await waitForPaint(['Original: 2', 'Deferred: 2']);
    
  347.       expect(root).toMatchRenderedOutput(
    
  348.         <div>
    
  349.           <div>Original: 2</div>
    
  350.           <div>Deferred: 2</div>
    
  351.         </div>,
    
  352.       );
    
  353.     });
    
  354. 
    
  355.     await act(async () => {
    
  356.       root.render(<App value={3} />);
    
  357.       await waitForPaint(['Original: 3']);
    
  358.       expect(root).toMatchRenderedOutput(
    
  359.         <div>
    
  360.           <div>Original: 3</div>
    
  361.           <div>Deferred: 2</div>
    
  362.         </div>,
    
  363.       );
    
  364.       await waitForPaint(['Deferred: 3']);
    
  365.       expect(root).toMatchRenderedOutput(
    
  366.         <div>
    
  367.           <div>Original: 3</div>
    
  368.           <div>Deferred: 3</div>
    
  369.         </div>,
    
  370.       );
    
  371.     });
    
  372.   });
    
  373. 
    
  374.   // @gate enableUseDeferredValueInitialArg
    
  375.   it('supports initialValue argument', async () => {
    
  376.     function App() {
    
  377.       const value = useDeferredValue('Final', 'Initial');
    
  378.       return <Text text={value} />;
    
  379.     }
    
  380. 
    
  381.     const root = ReactNoop.createRoot();
    
  382.     await act(async () => {
    
  383.       root.render(<App />);
    
  384.       await waitForPaint(['Initial']);
    
  385.       expect(root).toMatchRenderedOutput('Initial');
    
  386.     });
    
  387.     assertLog(['Final']);
    
  388.     expect(root).toMatchRenderedOutput('Final');
    
  389.   });
    
  390. 
    
  391.   // @gate enableUseDeferredValueInitialArg
    
  392.   it('defers during initial render when initialValue is provided, even if render is not sync', async () => {
    
  393.     function App() {
    
  394.       const value = useDeferredValue('Final', 'Initial');
    
  395.       return <Text text={value} />;
    
  396.     }
    
  397. 
    
  398.     const root = ReactNoop.createRoot();
    
  399.     await act(async () => {
    
  400.       // Initial mount is a transition, but it should defer anyway
    
  401.       startTransition(() => root.render(<App />));
    
  402.       await waitForPaint(['Initial']);
    
  403.       expect(root).toMatchRenderedOutput('Initial');
    
  404.     });
    
  405.     assertLog(['Final']);
    
  406.     expect(root).toMatchRenderedOutput('Final');
    
  407.   });
    
  408. 
    
  409.   // @gate enableUseDeferredValueInitialArg
    
  410.   it(
    
  411.     'if a suspended render spawns a deferred task, we can switch to the ' +
    
  412.       'deferred task without finishing the original one',
    
  413.     async () => {
    
  414.       function App() {
    
  415.         const text = useDeferredValue('Final', 'Loading...');
    
  416.         return <AsyncText text={text} />;
    
  417.       }
    
  418. 
    
  419.       const root = ReactNoop.createRoot();
    
  420.       await act(() => root.render(<App />));
    
  421.       assertLog([
    
  422.         'Suspend! [Loading...]',
    
  423.         // The initial value suspended, so we attempt the final value, which
    
  424.         // also suspends.
    
  425.         'Suspend! [Final]',
    
  426.       ]);
    
  427.       expect(root).toMatchRenderedOutput(null);
    
  428. 
    
  429.       // The final value loads, so we can skip the initial value entirely.
    
  430.       await act(() => resolveText('Final'));
    
  431.       assertLog(['Final']);
    
  432.       expect(root).toMatchRenderedOutput('Final');
    
  433. 
    
  434.       // When the initial value finally loads, nothing happens because we no
    
  435.       // longer need it.
    
  436.       await act(() => resolveText('Loading...'));
    
  437.       assertLog([]);
    
  438.       expect(root).toMatchRenderedOutput('Final');
    
  439.     },
    
  440.   );
    
  441. 
    
  442.   // @gate enableUseDeferredValueInitialArg
    
  443.   it(
    
  444.     'if a suspended render spawns a deferred task that also suspends, we can ' +
    
  445.       'finish the original task if that one loads first',
    
  446.     async () => {
    
  447.       function App() {
    
  448.         const text = useDeferredValue('Final', 'Loading...');
    
  449.         return <AsyncText text={text} />;
    
  450.       }
    
  451. 
    
  452.       const root = ReactNoop.createRoot();
    
  453.       await act(() => root.render(<App />));
    
  454.       assertLog([
    
  455.         'Suspend! [Loading...]',
    
  456.         // The initial value suspended, so we attempt the final value, which
    
  457.         // also suspends.
    
  458.         'Suspend! [Final]',
    
  459.       ]);
    
  460.       expect(root).toMatchRenderedOutput(null);
    
  461. 
    
  462.       // The initial value resolves first, so we render that.
    
  463.       await act(() => resolveText('Loading...'));
    
  464.       assertLog([
    
  465.         'Loading...',
    
  466.         // Still waiting for the final value.
    
  467.         'Suspend! [Final]',
    
  468.       ]);
    
  469.       expect(root).toMatchRenderedOutput('Loading...');
    
  470. 
    
  471.       // The final value loads, so we can switch to that.
    
  472.       await act(() => resolveText('Final'));
    
  473.       assertLog(['Final']);
    
  474.       expect(root).toMatchRenderedOutput('Final');
    
  475.     },
    
  476.   );
    
  477. 
    
  478.   // @gate enableUseDeferredValueInitialArg
    
  479.   it(
    
  480.     'if there are multiple useDeferredValues in the same tree, only the ' +
    
  481.       'first level defers; subsequent ones go straight to the final value, to ' +
    
  482.       'avoid a waterfall',
    
  483.     async () => {
    
  484.       function App() {
    
  485.         const showContent = useDeferredValue(true, false);
    
  486.         if (!showContent) {
    
  487.           return <Text text="App Preview" />;
    
  488.         }
    
  489.         return <Content />;
    
  490.       }
    
  491. 
    
  492.       function Content() {
    
  493.         const text = useDeferredValue('Content', 'Content Preview');
    
  494.         return <AsyncText text={text} />;
    
  495.       }
    
  496. 
    
  497.       const root = ReactNoop.createRoot();
    
  498.       resolveText('App Preview');
    
  499. 
    
  500.       await act(() => root.render(<App />));
    
  501.       assertLog([
    
  502.         // The App shows an immediate preview
    
  503.         'App Preview',
    
  504.         // Then we switch to showing the content. The Content component also
    
  505.         // contains a useDeferredValue, but since we already showed a preview
    
  506.         // in a parent component, we skip the preview in the inner one and
    
  507.         // go straight to attempting the final value.
    
  508.         //
    
  509.         // (Note that this is intentionally different from how nested Suspense
    
  510.         // boundaries work, where we always prefer to show the innermost
    
  511.         // loading state.)
    
  512.         'Suspend! [Content]',
    
  513.       ]);
    
  514.       // Still showing the App preview state because the inner
    
  515.       // content suspended.
    
  516.       expect(root).toMatchRenderedOutput('App Preview');
    
  517. 
    
  518.       // Finish loading the content
    
  519.       await act(() => resolveText('Content'));
    
  520.       // We didn't even attempt to render Content Preview.
    
  521.       assertLog(['Content']);
    
  522.       expect(root).toMatchRenderedOutput('Content');
    
  523.     },
    
  524.   );
    
  525. 
    
  526.   // @gate enableUseDeferredValueInitialArg
    
  527.   it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
    
  528.     // Same as the previous test but with a Suspense boundary separating the
    
  529.     // two useDeferredValue hooks.
    
  530.     function App() {
    
  531.       const showContent = useDeferredValue(true, false);
    
  532.       if (!showContent) {
    
  533.         return <Text text="App Preview" />;
    
  534.       }
    
  535.       return (
    
  536.         <Suspense fallback={<Text text="Loading..." />}>
    
  537.           <Content />
    
  538.         </Suspense>
    
  539.       );
    
  540.     }
    
  541. 
    
  542.     function Content() {
    
  543.       const text = useDeferredValue('Content', 'Content Preview');
    
  544.       return <AsyncText text={text} />;
    
  545.     }
    
  546. 
    
  547.     const root = ReactNoop.createRoot();
    
  548.     resolveText('App Preview');
    
  549. 
    
  550.     await act(() => root.render(<App />));
    
  551.     assertLog([
    
  552.       // The App shows an immediate preview
    
  553.       'App Preview',
    
  554.       // Then we switch to showing the content. The Content component also
    
  555.       // contains a useDeferredValue, but since we already showed a preview
    
  556.       // in a parent component, we skip the preview in the inner one and
    
  557.       // go straight to attempting the final value.
    
  558.       'Suspend! [Content]',
    
  559.       'Loading...',
    
  560.     ]);
    
  561.     // The content suspended, so we show a Suspense fallback
    
  562.     expect(root).toMatchRenderedOutput('Loading...');
    
  563. 
    
  564.     // Finish loading the content
    
  565.     await act(() => resolveText('Content'));
    
  566.     // We didn't even attempt to render Content Preview.
    
  567.     assertLog(['Content']);
    
  568.     expect(root).toMatchRenderedOutput('Content');
    
  569.   });
    
  570. 
    
  571.   // @gate enableUseDeferredValueInitialArg
    
  572.   // @gate enableActivity
    
  573.   it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => {
    
  574.     function App() {
    
  575.       const text = useDeferredValue('Final', 'Preview');
    
  576.       return (
    
  577.         <div>
    
  578.           <AsyncText text={text} />
    
  579.         </div>
    
  580.       );
    
  581.     }
    
  582. 
    
  583.     let revealContent;
    
  584.     function Container({children}) {
    
  585.       const [shouldShow, setState] = useState(false);
    
  586.       revealContent = () => setState(true);
    
  587.       return (
    
  588.         <Activity mode={shouldShow ? 'visible' : 'hidden'}>{children}</Activity>
    
  589.       );
    
  590.     }
    
  591. 
    
  592.     const root = ReactNoop.createRoot();
    
  593. 
    
  594.     // Prerender a hidden tree
    
  595.     resolveText('Preview');
    
  596.     await act(() =>
    
  597.       root.render(
    
  598.         <Container>
    
  599.           <App />
    
  600.         </Container>,
    
  601.       ),
    
  602.     );
    
  603.     assertLog(['Preview', 'Suspend! [Final]']);
    
  604.     expect(root).toMatchRenderedOutput(<div hidden={true}>Preview</div>);
    
  605. 
    
  606.     // Finish loading the content
    
  607.     await act(() => resolveText('Final'));
    
  608.     assertLog(['Final']);
    
  609.     expect(root).toMatchRenderedOutput(<div hidden={true}>Final</div>);
    
  610. 
    
  611.     // Now reveal the hidden tree. It should toggle the visibility without
    
  612.     // having to re-render anything inside the prerendered tree.
    
  613.     await act(() => revealContent());
    
  614.     assertLog([]);
    
  615.     expect(root).toMatchRenderedOutput(<div>Final</div>);
    
  616.   });
    
  617. 
    
  618.   // @gate enableUseDeferredValueInitialArg
    
  619.   // @gate enableActivity
    
  620.   it('useDeferredValue can prerender the initial value inside a hidden tree', async () => {
    
  621.     function App({text}) {
    
  622.       const renderedText = useDeferredValue(text, `Preview [${text}]`);
    
  623.       return (
    
  624.         <div>
    
  625.           <Text text={renderedText} />
    
  626.         </div>
    
  627.       );
    
  628.     }
    
  629. 
    
  630.     let revealContent;
    
  631.     function Container({children}) {
    
  632.       const [shouldShow, setState] = useState(false);
    
  633.       revealContent = () => setState(true);
    
  634.       return (
    
  635.         <Activity mode={shouldShow ? 'visible' : 'hidden'}>{children}</Activity>
    
  636.       );
    
  637.     }
    
  638. 
    
  639.     const root = ReactNoop.createRoot();
    
  640. 
    
  641.     // Prerender some content
    
  642.     await act(() => {
    
  643.       root.render(
    
  644.         <Container>
    
  645.           <App text="A" />
    
  646.         </Container>,
    
  647.       );
    
  648.     });
    
  649.     assertLog(['Preview [A]', 'A']);
    
  650.     expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
    
  651. 
    
  652.     await act(async () => {
    
  653.       // While the tree is still hidden, update the pre-rendered tree.
    
  654.       root.render(
    
  655.         <Container>
    
  656.           <App text="B" />
    
  657.         </Container>,
    
  658.       );
    
  659.       // We should switch to pre-rendering the new preview.
    
  660.       await waitForPaint(['Preview [B]']);
    
  661.       expect(root).toMatchRenderedOutput(<div hidden={true}>Preview [B]</div>);
    
  662. 
    
  663.       // Before the prerender is complete, reveal the hidden tree. Because we
    
  664.       // consider revealing a hidden tree to be the same as mounting a new one,
    
  665.       // we should not skip the preview state.
    
  666.       revealContent();
    
  667.       // Because the preview state was already prerendered, we can reveal it
    
  668.       // without any addditional work.
    
  669.       await waitForPaint([]);
    
  670.       expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
    
  671.     });
    
  672.     // Finally, finish rendering the final value.
    
  673.     assertLog(['B']);
    
  674.     expect(root).toMatchRenderedOutput(<div>B</div>);
    
  675.   });
    
  676. 
    
  677.   // @gate enableUseDeferredValueInitialArg
    
  678.   // @gate enableActivity
    
  679.   it(
    
  680.     'useDeferredValue skips the preview state when revealing a hidden tree ' +
    
  681.       'if the final value is referentially identical',
    
  682.     async () => {
    
  683.       function App({text}) {
    
  684.         const renderedText = useDeferredValue(text, `Preview [${text}]`);
    
  685.         return (
    
  686.           <div>
    
  687.             <Text text={renderedText} />
    
  688.           </div>
    
  689.         );
    
  690.       }
    
  691. 
    
  692.       function Container({text, shouldShow}) {
    
  693.         return (
    
  694.           <Activity mode={shouldShow ? 'visible' : 'hidden'}>
    
  695.             <App text={text} />
    
  696.           </Activity>
    
  697.         );
    
  698.       }
    
  699. 
    
  700.       const root = ReactNoop.createRoot();
    
  701. 
    
  702.       // Prerender some content
    
  703.       await act(() => root.render(<Container text="A" shouldShow={false} />));
    
  704.       assertLog(['Preview [A]', 'A']);
    
  705.       expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
    
  706. 
    
  707.       // Reveal the prerendered tree. Because the final value is referentially
    
  708.       // equal to what was already prerendered, we can skip the preview state
    
  709.       // and go straight to the final one. The practical upshot of this is
    
  710.       // that we can completely prerender the final value without having to
    
  711.       // do additional rendering work when the tree is revealed.
    
  712.       await act(() => root.render(<Container text="A" shouldShow={true} />));
    
  713.       assertLog(['A']);
    
  714.       expect(root).toMatchRenderedOutput(<div>A</div>);
    
  715.     },
    
  716.   );
    
  717. 
    
  718.   // @gate enableUseDeferredValueInitialArg
    
  719.   // @gate enableActivity
    
  720.   it(
    
  721.     'useDeferredValue does not skip the preview state when revealing a ' +
    
  722.       'hidden tree if the final value is different from the currently rendered one',
    
  723.     async () => {
    
  724.       function App({text}) {
    
  725.         const renderedText = useDeferredValue(text, `Preview [${text}]`);
    
  726.         return (
    
  727.           <div>
    
  728.             <Text text={renderedText} />
    
  729.           </div>
    
  730.         );
    
  731.       }
    
  732. 
    
  733.       function Container({text, shouldShow}) {
    
  734.         return (
    
  735.           <Activity mode={shouldShow ? 'visible' : 'hidden'}>
    
  736.             <App text={text} />
    
  737.           </Activity>
    
  738.         );
    
  739.       }
    
  740. 
    
  741.       const root = ReactNoop.createRoot();
    
  742. 
    
  743.       // Prerender some content
    
  744.       await act(() => root.render(<Container text="A" shouldShow={false} />));
    
  745.       assertLog(['Preview [A]', 'A']);
    
  746.       expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
    
  747. 
    
  748.       // Reveal the prerendered tree. Because the final value is different from
    
  749.       // what was already prerendered, we can't bail out. Since we treat
    
  750.       // revealing a hidden tree the same as a new mount, show the preview state
    
  751.       // before switching to the final one.
    
  752.       await act(async () => {
    
  753.         root.render(<Container text="B" shouldShow={true} />);
    
  754.         // First commit the preview state
    
  755.         await waitForPaint(['Preview [B]']);
    
  756.         expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
    
  757.       });
    
  758.       // Then switch to the final state
    
  759.       assertLog(['B']);
    
  760.       expect(root).toMatchRenderedOutput(<div>B</div>);
    
  761.     },
    
  762.   );
    
  763. 
    
  764.   // @gate enableActivity
    
  765.   it(
    
  766.     'useDeferredValue does not show "previous" value when revealing a hidden ' +
    
  767.       'tree (no initial value)',
    
  768.     async () => {
    
  769.       function App({text}) {
    
  770.         const renderedText = useDeferredValue(text);
    
  771.         return (
    
  772.           <div>
    
  773.             <Text text={renderedText} />
    
  774.           </div>
    
  775.         );
    
  776.       }
    
  777. 
    
  778.       function Container({text, shouldShow}) {
    
  779.         return (
    
  780.           <Activity mode={shouldShow ? 'visible' : 'hidden'}>
    
  781.             <App text={text} />
    
  782.           </Activity>
    
  783.         );
    
  784.       }
    
  785. 
    
  786.       const root = ReactNoop.createRoot();
    
  787. 
    
  788.       // Prerender some content
    
  789.       await act(() => root.render(<Container text="A" shouldShow={false} />));
    
  790.       assertLog(['A']);
    
  791.       expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
    
  792. 
    
  793.       // Update the prerendered tree and reveal it at the same time. Even though
    
  794.       // this is a sync update, we should update B immediately rather than stay
    
  795.       // on the old value (A), because conceptually this is a new tree.
    
  796.       await act(() => root.render(<Container text="B" shouldShow={true} />));
    
  797.       assertLog(['B']);
    
  798.       expect(root).toMatchRenderedOutput(<div>B</div>);
    
  799.     },
    
  800.   );
    
  801. });