1. let React;
    
  2. let ReactNoop;
    
  3. let Scheduler;
    
  4. let act;
    
  5. let assertLog;
    
  6. let useTransition;
    
  7. let useState;
    
  8. let useOptimistic;
    
  9. let textCache;
    
  10. 
    
  11. describe('ReactAsyncActions', () => {
    
  12.   beforeEach(() => {
    
  13.     jest.resetModules();
    
  14. 
    
  15.     React = require('react');
    
  16.     ReactNoop = require('react-noop-renderer');
    
  17.     Scheduler = require('scheduler');
    
  18.     act = require('internal-test-utils').act;
    
  19.     assertLog = require('internal-test-utils').assertLog;
    
  20.     useTransition = React.useTransition;
    
  21.     useState = React.useState;
    
  22.     useOptimistic = React.useOptimistic;
    
  23. 
    
  24.     textCache = new Map();
    
  25.   });
    
  26. 
    
  27.   function resolveText(text) {
    
  28.     const record = textCache.get(text);
    
  29.     if (record === undefined) {
    
  30.       const newRecord = {
    
  31.         status: 'resolved',
    
  32.         value: text,
    
  33.       };
    
  34.       textCache.set(text, newRecord);
    
  35.     } else if (record.status === 'pending') {
    
  36.       const thenable = record.value;
    
  37.       record.status = 'resolved';
    
  38.       record.value = text;
    
  39.       thenable.pings.forEach(t => t());
    
  40.     }
    
  41.   }
    
  42. 
    
  43.   function readText(text) {
    
  44.     const record = textCache.get(text);
    
  45.     if (record !== undefined) {
    
  46.       switch (record.status) {
    
  47.         case 'pending':
    
  48.           Scheduler.log(`Suspend! [${text}]`);
    
  49.           throw record.value;
    
  50.         case 'rejected':
    
  51.           throw record.value;
    
  52.         case 'resolved':
    
  53.           return record.value;
    
  54.       }
    
  55.     } else {
    
  56.       Scheduler.log(`Suspend! [${text}]`);
    
  57.       const thenable = {
    
  58.         pings: [],
    
  59.         then(resolve) {
    
  60.           if (newRecord.status === 'pending') {
    
  61.             thenable.pings.push(resolve);
    
  62.           } else {
    
  63.             Promise.resolve().then(() => resolve(newRecord.value));
    
  64.           }
    
  65.         },
    
  66.       };
    
  67. 
    
  68.       const newRecord = {
    
  69.         status: 'pending',
    
  70.         value: thenable,
    
  71.       };
    
  72.       textCache.set(text, newRecord);
    
  73. 
    
  74.       throw thenable;
    
  75.     }
    
  76.   }
    
  77. 
    
  78.   function getText(text) {
    
  79.     const record = textCache.get(text);
    
  80.     if (record === undefined) {
    
  81.       const thenable = {
    
  82.         pings: [],
    
  83.         then(resolve) {
    
  84.           if (newRecord.status === 'pending') {
    
  85.             thenable.pings.push(resolve);
    
  86.           } else {
    
  87.             Promise.resolve().then(() => resolve(newRecord.value));
    
  88.           }
    
  89.         },
    
  90.       };
    
  91.       const newRecord = {
    
  92.         status: 'pending',
    
  93.         value: thenable,
    
  94.       };
    
  95.       textCache.set(text, newRecord);
    
  96.       return thenable;
    
  97.     } else {
    
  98.       switch (record.status) {
    
  99.         case 'pending':
    
  100.           return record.value;
    
  101.         case 'rejected':
    
  102.           return Promise.reject(record.value);
    
  103.         case 'resolved':
    
  104.           return Promise.resolve(record.value);
    
  105.       }
    
  106.     }
    
  107.   }
    
  108. 
    
  109.   function Text({text}) {
    
  110.     Scheduler.log(text);
    
  111.     return text;
    
  112.   }
    
  113. 
    
  114.   function AsyncText({text}) {
    
  115.     readText(text);
    
  116.     Scheduler.log(text);
    
  117.     return text;
    
  118.   }
    
  119. 
    
  120.   // @gate enableAsyncActions
    
  121.   test('isPending remains true until async action finishes', async () => {
    
  122.     let startTransition;
    
  123.     function App() {
    
  124.       const [isPending, _start] = useTransition();
    
  125.       startTransition = _start;
    
  126.       return <Text text={'Pending: ' + isPending} />;
    
  127.     }
    
  128. 
    
  129.     const root = ReactNoop.createRoot();
    
  130.     await act(() => {
    
  131.       root.render(<App />);
    
  132.     });
    
  133.     assertLog(['Pending: false']);
    
  134.     expect(root).toMatchRenderedOutput('Pending: false');
    
  135. 
    
  136.     // At the start of an async action, isPending is set to true.
    
  137.     await act(() => {
    
  138.       startTransition(async () => {
    
  139.         Scheduler.log('Async action started');
    
  140.         await getText('Wait');
    
  141.         Scheduler.log('Async action ended');
    
  142.       });
    
  143.     });
    
  144.     assertLog(['Async action started', 'Pending: true']);
    
  145.     expect(root).toMatchRenderedOutput('Pending: true');
    
  146. 
    
  147.     // Once the action finishes, isPending is set back to false.
    
  148.     await act(() => resolveText('Wait'));
    
  149.     assertLog(['Async action ended', 'Pending: false']);
    
  150.     expect(root).toMatchRenderedOutput('Pending: false');
    
  151.   });
    
  152. 
    
  153.   // @gate enableAsyncActions
    
  154.   test('multiple updates in an async action scope are entangled together', async () => {
    
  155.     let startTransition;
    
  156.     function App({text}) {
    
  157.       const [isPending, _start] = useTransition();
    
  158.       startTransition = _start;
    
  159.       return (
    
  160.         <>
    
  161.           <span>
    
  162.             <Text text={'Pending: ' + isPending} />
    
  163.           </span>
    
  164.           <span>
    
  165.             <Text text={text} />
    
  166.           </span>
    
  167.         </>
    
  168.       );
    
  169.     }
    
  170. 
    
  171.     const root = ReactNoop.createRoot();
    
  172.     await act(() => {
    
  173.       root.render(<App text="A" />);
    
  174.     });
    
  175.     assertLog(['Pending: false', 'A']);
    
  176.     expect(root).toMatchRenderedOutput(
    
  177.       <>
    
  178.         <span>Pending: false</span>
    
  179.         <span>A</span>
    
  180.       </>,
    
  181.     );
    
  182. 
    
  183.     await act(() => {
    
  184.       startTransition(async () => {
    
  185.         Scheduler.log('Async action started');
    
  186.         await getText('Yield before updating');
    
  187.         Scheduler.log('Async action ended');
    
  188.         startTransition(() => root.render(<App text="B" />));
    
  189.       });
    
  190.     });
    
  191.     assertLog(['Async action started', 'Pending: true', 'A']);
    
  192.     expect(root).toMatchRenderedOutput(
    
  193.       <>
    
  194.         <span>Pending: true</span>
    
  195.         <span>A</span>
    
  196.       </>,
    
  197.     );
    
  198. 
    
  199.     await act(() => resolveText('Yield before updating'));
    
  200.     assertLog(['Async action ended', 'Pending: false', 'B']);
    
  201.     expect(root).toMatchRenderedOutput(
    
  202.       <>
    
  203.         <span>Pending: false</span>
    
  204.         <span>B</span>
    
  205.       </>,
    
  206.     );
    
  207.   });
    
  208. 
    
  209.   // @gate enableAsyncActions
    
  210.   test('multiple async action updates in the same scope are entangled together', async () => {
    
  211.     let setStepA;
    
  212.     function A() {
    
  213.       const [step, setStep] = useState(0);
    
  214.       setStepA = setStep;
    
  215.       return <AsyncText text={'A' + step} />;
    
  216.     }
    
  217. 
    
  218.     let setStepB;
    
  219.     function B() {
    
  220.       const [step, setStep] = useState(0);
    
  221.       setStepB = setStep;
    
  222.       return <AsyncText text={'B' + step} />;
    
  223.     }
    
  224. 
    
  225.     let setStepC;
    
  226.     function C() {
    
  227.       const [step, setStep] = useState(0);
    
  228.       setStepC = setStep;
    
  229.       return <AsyncText text={'C' + step} />;
    
  230.     }
    
  231. 
    
  232.     let startTransition;
    
  233.     function App() {
    
  234.       const [isPending, _start] = useTransition();
    
  235.       startTransition = _start;
    
  236.       return (
    
  237.         <>
    
  238.           <span>
    
  239.             <Text text={'Pending: ' + isPending} />
    
  240.           </span>
    
  241.           <span>
    
  242.             <A />, <B />, <C />
    
  243.           </span>
    
  244.         </>
    
  245.       );
    
  246.     }
    
  247. 
    
  248.     const root = ReactNoop.createRoot();
    
  249.     resolveText('A0');
    
  250.     resolveText('B0');
    
  251.     resolveText('C0');
    
  252.     await act(() => {
    
  253.       root.render(<App text="A" />);
    
  254.     });
    
  255.     assertLog(['Pending: false', 'A0', 'B0', 'C0']);
    
  256.     expect(root).toMatchRenderedOutput(
    
  257.       <>
    
  258.         <span>Pending: false</span>
    
  259.         <span>A0, B0, C0</span>
    
  260.       </>,
    
  261.     );
    
  262. 
    
  263.     await act(() => {
    
  264.       startTransition(async () => {
    
  265.         Scheduler.log('Async action started');
    
  266.         setStepA(1);
    
  267.         await getText('Wait before updating B');
    
  268.         startTransition(() => setStepB(1));
    
  269.         await getText('Wait before updating C');
    
  270.         startTransition(() => setStepC(1));
    
  271.         Scheduler.log('Async action ended');
    
  272.       });
    
  273.     });
    
  274.     assertLog(['Async action started', 'Pending: true', 'A0', 'B0', 'C0']);
    
  275.     expect(root).toMatchRenderedOutput(
    
  276.       <>
    
  277.         <span>Pending: true</span>
    
  278.         <span>A0, B0, C0</span>
    
  279.       </>,
    
  280.     );
    
  281. 
    
  282.     // This will schedule an update on B, but nothing will render yet because
    
  283.     // the async action scope hasn't finished.
    
  284.     await act(() => resolveText('Wait before updating B'));
    
  285.     assertLog([]);
    
  286.     expect(root).toMatchRenderedOutput(
    
  287.       <>
    
  288.         <span>Pending: true</span>
    
  289.         <span>A0, B0, C0</span>
    
  290.       </>,
    
  291.     );
    
  292. 
    
  293.     // This will schedule an update on C, and also the async action scope
    
  294.     // will end. This will allow React to attempt to render the updates.
    
  295.     await act(() => resolveText('Wait before updating C'));
    
  296.     assertLog(['Async action ended', 'Pending: false', 'Suspend! [A1]']);
    
  297.     expect(root).toMatchRenderedOutput(
    
  298.       <>
    
  299.         <span>Pending: true</span>
    
  300.         <span>A0, B0, C0</span>
    
  301.       </>,
    
  302.     );
    
  303. 
    
  304.     // Progressively load the all the data. Because they are all entangled
    
  305.     // together, only when the all of A, B, and C updates are unblocked is the
    
  306.     // render allowed to proceed.
    
  307.     await act(() => resolveText('A1'));
    
  308.     assertLog(['Pending: false', 'A1', 'Suspend! [B1]']);
    
  309.     expect(root).toMatchRenderedOutput(
    
  310.       <>
    
  311.         <span>Pending: true</span>
    
  312.         <span>A0, B0, C0</span>
    
  313.       </>,
    
  314.     );
    
  315.     await act(() => resolveText('B1'));
    
  316.     assertLog(['Pending: false', 'A1', 'B1', 'Suspend! [C1]']);
    
  317.     expect(root).toMatchRenderedOutput(
    
  318.       <>
    
  319.         <span>Pending: true</span>
    
  320.         <span>A0, B0, C0</span>
    
  321.       </>,
    
  322.     );
    
  323. 
    
  324.     // Finally, all the data has loaded and the transition is complete.
    
  325.     await act(() => resolveText('C1'));
    
  326.     assertLog(['Pending: false', 'A1', 'B1', 'C1']);
    
  327.     expect(root).toMatchRenderedOutput(
    
  328.       <>
    
  329.         <span>Pending: false</span>
    
  330.         <span>A1, B1, C1</span>
    
  331.       </>,
    
  332.     );
    
  333.   });
    
  334. 
    
  335.   // @gate enableAsyncActions
    
  336.   test('urgent updates are not blocked during an async action', async () => {
    
  337.     let setStepA;
    
  338.     function A() {
    
  339.       const [step, setStep] = useState(0);
    
  340.       setStepA = setStep;
    
  341.       return <Text text={'A' + step} />;
    
  342.     }
    
  343. 
    
  344.     let setStepB;
    
  345.     function B() {
    
  346.       const [step, setStep] = useState(0);
    
  347.       setStepB = setStep;
    
  348.       return <Text text={'B' + step} />;
    
  349.     }
    
  350. 
    
  351.     let startTransition;
    
  352.     function App() {
    
  353.       const [isPending, _start] = useTransition();
    
  354.       startTransition = _start;
    
  355.       return (
    
  356.         <>
    
  357.           <span>
    
  358.             <Text text={'Pending: ' + isPending} />
    
  359.           </span>
    
  360.           <span>
    
  361.             <A />, <B />
    
  362.           </span>
    
  363.         </>
    
  364.       );
    
  365.     }
    
  366. 
    
  367.     const root = ReactNoop.createRoot();
    
  368.     await act(() => {
    
  369.       root.render(<App text="A" />);
    
  370.     });
    
  371.     assertLog(['Pending: false', 'A0', 'B0']);
    
  372.     expect(root).toMatchRenderedOutput(
    
  373.       <>
    
  374.         <span>Pending: false</span>
    
  375.         <span>A0, B0</span>
    
  376.       </>,
    
  377.     );
    
  378. 
    
  379.     await act(() => {
    
  380.       startTransition(async () => {
    
  381.         Scheduler.log('Async action started');
    
  382.         startTransition(() => setStepA(1));
    
  383.         await getText('Wait');
    
  384.         Scheduler.log('Async action ended');
    
  385.       });
    
  386.     });
    
  387.     assertLog(['Async action started', 'Pending: true', 'A0', 'B0']);
    
  388.     expect(root).toMatchRenderedOutput(
    
  389.       <>
    
  390.         <span>Pending: true</span>
    
  391.         <span>A0, B0</span>
    
  392.       </>,
    
  393.     );
    
  394. 
    
  395.     // Update B at urgent priority. This should be allowed to finish.
    
  396.     await act(() => setStepB(1));
    
  397.     assertLog(['B1']);
    
  398.     expect(root).toMatchRenderedOutput(
    
  399.       <>
    
  400.         <span>Pending: true</span>
    
  401.         <span>A0, B1</span>
    
  402.       </>,
    
  403.     );
    
  404. 
    
  405.     // Finish the async action.
    
  406.     await act(() => resolveText('Wait'));
    
  407.     assertLog(['Async action ended', 'Pending: false', 'A1', 'B1']);
    
  408.     expect(root).toMatchRenderedOutput(
    
  409.       <>
    
  410.         <span>Pending: false</span>
    
  411.         <span>A1, B1</span>
    
  412.       </>,
    
  413.     );
    
  414.   });
    
  415. 
    
  416.   // @gate enableAsyncActions
    
  417.   test("if a sync action throws, it's rethrown from the `useTransition`", async () => {
    
  418.     class ErrorBoundary extends React.Component {
    
  419.       state = {error: null};
    
  420.       static getDerivedStateFromError(error) {
    
  421.         return {error};
    
  422.       }
    
  423.       render() {
    
  424.         if (this.state.error) {
    
  425.           return <Text text={this.state.error.message} />;
    
  426.         }
    
  427.         return this.props.children;
    
  428.       }
    
  429.     }
    
  430. 
    
  431.     let startTransition;
    
  432.     function App() {
    
  433.       const [isPending, _start] = useTransition();
    
  434.       startTransition = _start;
    
  435.       return <Text text={'Pending: ' + isPending} />;
    
  436.     }
    
  437. 
    
  438.     const root = ReactNoop.createRoot();
    
  439.     await act(() => {
    
  440.       root.render(
    
  441.         <ErrorBoundary>
    
  442.           <App />
    
  443.         </ErrorBoundary>,
    
  444.       );
    
  445.     });
    
  446.     assertLog(['Pending: false']);
    
  447.     expect(root).toMatchRenderedOutput('Pending: false');
    
  448. 
    
  449.     await act(() => {
    
  450.       startTransition(() => {
    
  451.         throw new Error('Oops!');
    
  452.       });
    
  453.     });
    
  454.     assertLog(['Pending: true', 'Oops!', 'Oops!']);
    
  455.     expect(root).toMatchRenderedOutput('Oops!');
    
  456.   });
    
  457. 
    
  458.   // @gate enableAsyncActions
    
  459.   test("if an async action throws, it's rethrown from the `useTransition`", async () => {
    
  460.     class ErrorBoundary extends React.Component {
    
  461.       state = {error: null};
    
  462.       static getDerivedStateFromError(error) {
    
  463.         return {error};
    
  464.       }
    
  465.       render() {
    
  466.         if (this.state.error) {
    
  467.           return <Text text={this.state.error.message} />;
    
  468.         }
    
  469.         return this.props.children;
    
  470.       }
    
  471.     }
    
  472. 
    
  473.     let startTransition;
    
  474.     function App() {
    
  475.       const [isPending, _start] = useTransition();
    
  476.       startTransition = _start;
    
  477.       return <Text text={'Pending: ' + isPending} />;
    
  478.     }
    
  479. 
    
  480.     const root = ReactNoop.createRoot();
    
  481.     await act(() => {
    
  482.       root.render(
    
  483.         <ErrorBoundary>
    
  484.           <App />
    
  485.         </ErrorBoundary>,
    
  486.       );
    
  487.     });
    
  488.     assertLog(['Pending: false']);
    
  489.     expect(root).toMatchRenderedOutput('Pending: false');
    
  490. 
    
  491.     await act(() => {
    
  492.       startTransition(async () => {
    
  493.         Scheduler.log('Async action started');
    
  494.         await getText('Wait');
    
  495.         throw new Error('Oops!');
    
  496.       });
    
  497.     });
    
  498.     assertLog(['Async action started', 'Pending: true']);
    
  499.     expect(root).toMatchRenderedOutput('Pending: true');
    
  500. 
    
  501.     await act(() => resolveText('Wait'));
    
  502.     assertLog(['Oops!', 'Oops!']);
    
  503.     expect(root).toMatchRenderedOutput('Oops!');
    
  504.   });
    
  505. 
    
  506.   // @gate !enableAsyncActions
    
  507.   test('when enableAsyncActions is disabled, and a sync action throws, `isPending` is turned off', async () => {
    
  508.     let startTransition;
    
  509.     function App() {
    
  510.       const [isPending, _start] = useTransition();
    
  511.       startTransition = _start;
    
  512.       return <Text text={'Pending: ' + isPending} />;
    
  513.     }
    
  514. 
    
  515.     const root = ReactNoop.createRoot();
    
  516.     await act(() => {
    
  517.       root.render(<App />);
    
  518.     });
    
  519.     assertLog(['Pending: false']);
    
  520.     expect(root).toMatchRenderedOutput('Pending: false');
    
  521. 
    
  522.     await act(() => {
    
  523.       expect(() => {
    
  524.         startTransition(() => {
    
  525.           throw new Error('Oops!');
    
  526.         });
    
  527.       }).toThrow('Oops!');
    
  528.     });
    
  529.     assertLog(['Pending: true', 'Pending: false']);
    
  530.     expect(root).toMatchRenderedOutput('Pending: false');
    
  531.   });
    
  532. 
    
  533.   // @gate enableAsyncActions
    
  534.   test('if there are multiple entangled actions, and one of them errors, it only affects that action', async () => {
    
  535.     class ErrorBoundary extends React.Component {
    
  536.       state = {error: null};
    
  537.       static getDerivedStateFromError(error) {
    
  538.         return {error};
    
  539.       }
    
  540.       render() {
    
  541.         if (this.state.error) {
    
  542.           return <Text text={this.state.error.message} />;
    
  543.         }
    
  544.         return this.props.children;
    
  545.       }
    
  546.     }
    
  547. 
    
  548.     let startTransitionA;
    
  549.     function ActionA() {
    
  550.       const [isPendingA, start] = useTransition();
    
  551.       startTransitionA = start;
    
  552.       return <Text text={'Pending A: ' + isPendingA} />;
    
  553.     }
    
  554. 
    
  555.     let startTransitionB;
    
  556.     function ActionB() {
    
  557.       const [isPending, start] = useTransition();
    
  558.       startTransitionB = start;
    
  559.       return <Text text={'Pending B: ' + isPending} />;
    
  560.     }
    
  561. 
    
  562.     let startTransitionC;
    
  563.     function ActionC() {
    
  564.       const [isPending, start] = useTransition();
    
  565.       startTransitionC = start;
    
  566.       return <Text text={'Pending C: ' + isPending} />;
    
  567.     }
    
  568. 
    
  569.     const root = ReactNoop.createRoot();
    
  570.     await act(() => {
    
  571.       root.render(
    
  572.         <>
    
  573.           <div>
    
  574.             <ErrorBoundary>
    
  575.               <ActionA />
    
  576.             </ErrorBoundary>
    
  577.           </div>
    
  578.           <div>
    
  579.             <ErrorBoundary>
    
  580.               <ActionB />
    
  581.             </ErrorBoundary>
    
  582.           </div>
    
  583.           <div>
    
  584.             <ErrorBoundary>
    
  585.               <ActionC />
    
  586.             </ErrorBoundary>
    
  587.           </div>
    
  588.         </>,
    
  589.       );
    
  590.     });
    
  591.     assertLog(['Pending A: false', 'Pending B: false', 'Pending C: false']);
    
  592.     expect(root).toMatchRenderedOutput(
    
  593.       <>
    
  594.         <div>Pending A: false</div>
    
  595.         <div>Pending B: false</div>
    
  596.         <div>Pending C: false</div>
    
  597.       </>,
    
  598.     );
    
  599. 
    
  600.     // Start a bunch of entangled transitions. A and C throw errors, but B
    
  601.     // doesn't. A and should surface their respective errors, but B should
    
  602.     // finish successfully.
    
  603.     await act(() => {
    
  604.       startTransitionC(async () => {
    
  605.         startTransitionB(async () => {
    
  606.           startTransitionA(async () => {
    
  607.             await getText('Wait for A');
    
  608.             throw new Error('Oops A!');
    
  609.           });
    
  610.           await getText('Wait for B');
    
  611.         });
    
  612.         await getText('Wait for C');
    
  613.         throw new Error('Oops C!');
    
  614.       });
    
  615.     });
    
  616.     assertLog(['Pending A: true', 'Pending B: true', 'Pending C: true']);
    
  617. 
    
  618.     // Finish action A. We can't commit the result yet because it's entangled
    
  619.     // with B and C.
    
  620.     await act(() => resolveText('Wait for A'));
    
  621.     assertLog([]);
    
  622. 
    
  623.     // Finish action B. Same as above.
    
  624.     await act(() => resolveText('Wait for B'));
    
  625.     assertLog([]);
    
  626. 
    
  627.     // Now finish action C. This is the last action in the entangled set, so
    
  628.     // rendering can proceed.
    
  629.     await act(() => resolveText('Wait for C'));
    
  630.     assertLog([
    
  631.       // A and C result in (separate) errors, but B does not.
    
  632.       'Oops A!',
    
  633.       'Pending B: false',
    
  634.       'Oops C!',
    
  635. 
    
  636.       // Because there was an error, React will try rendering one more time.
    
  637.       'Oops A!',
    
  638.       'Pending B: false',
    
  639.       'Oops C!',
    
  640.     ]);
    
  641.     expect(root).toMatchRenderedOutput(
    
  642.       <>
    
  643.         <div>Oops A!</div>
    
  644.         <div>Pending B: false</div>
    
  645.         <div>Oops C!</div>
    
  646.       </>,
    
  647.     );
    
  648.   });
    
  649. 
    
  650.   // @gate enableAsyncActions
    
  651.   test('useOptimistic can be used to implement a pending state', async () => {
    
  652.     const startTransition = React.startTransition;
    
  653. 
    
  654.     let setIsPending;
    
  655.     function App({text}) {
    
  656.       const [isPending, _setIsPending] = useOptimistic(false);
    
  657.       setIsPending = _setIsPending;
    
  658.       return (
    
  659.         <>
    
  660.           <Text text={'Pending: ' + isPending} />
    
  661.           <AsyncText text={text} />
    
  662.         </>
    
  663.       );
    
  664.     }
    
  665. 
    
  666.     // Initial render
    
  667.     const root = ReactNoop.createRoot();
    
  668.     resolveText('A');
    
  669.     await act(() => root.render(<App text="A" />));
    
  670.     assertLog(['Pending: false', 'A']);
    
  671.     expect(root).toMatchRenderedOutput('Pending: falseA');
    
  672. 
    
  673.     // Start a transition
    
  674.     await act(() =>
    
  675.       startTransition(() => {
    
  676.         setIsPending(true);
    
  677.         root.render(<App text="B" />);
    
  678.       }),
    
  679.     );
    
  680.     assertLog([
    
  681.       // Render the pending state immediately
    
  682.       'Pending: true',
    
  683.       'A',
    
  684. 
    
  685.       // Then attempt to render the transition. The pending state will be
    
  686.       // automatically reverted.
    
  687.       'Pending: false',
    
  688.       'Suspend! [B]',
    
  689.     ]);
    
  690. 
    
  691.     // Resolve the transition
    
  692.     await act(() => resolveText('B'));
    
  693.     assertLog([
    
  694.       // Render the pending state immediately
    
  695.       'Pending: false',
    
  696.       'B',
    
  697.     ]);
    
  698.   });
    
  699. 
    
  700.   // @gate enableAsyncActions
    
  701.   test('useOptimistic rebases pending updates on top of passthrough value', async () => {
    
  702.     let serverCart = ['A'];
    
  703. 
    
  704.     async function submitNewItem(item) {
    
  705.       await getText('Adding item ' + item);
    
  706.       serverCart = [...serverCart, item];
    
  707.       React.startTransition(() => {
    
  708.         root.render(<App cart={serverCart} />);
    
  709.       });
    
  710.     }
    
  711. 
    
  712.     let addItemToCart;
    
  713.     function App({cart}) {
    
  714.       const [isPending, startTransition] = useTransition();
    
  715. 
    
  716.       const savedCartSize = cart.length;
    
  717.       const [optimisticCartSize, setOptimisticCartSize] =
    
  718.         useOptimistic(savedCartSize);
    
  719. 
    
  720.       addItemToCart = item => {
    
  721.         startTransition(async () => {
    
  722.           setOptimisticCartSize(n => n + 1);
    
  723.           await submitNewItem(item);
    
  724.         });
    
  725.       };
    
  726. 
    
  727.       return (
    
  728.         <>
    
  729.           <div>
    
  730.             <Text text={'Pending: ' + isPending} />
    
  731.           </div>
    
  732.           <div>
    
  733.             <Text text={'Items in cart: ' + optimisticCartSize} />
    
  734.           </div>
    
  735.           <ul>
    
  736.             {cart.map(item => (
    
  737.               <li key={item}>
    
  738.                 <Text text={'Item ' + item} />
    
  739.               </li>
    
  740.             ))}
    
  741.           </ul>
    
  742.         </>
    
  743.       );
    
  744.     }
    
  745. 
    
  746.     // Initial render
    
  747.     const root = ReactNoop.createRoot();
    
  748.     await act(() => root.render(<App cart={serverCart} />));
    
  749.     assertLog(['Pending: false', 'Items in cart: 1', 'Item A']);
    
  750.     expect(root).toMatchRenderedOutput(
    
  751.       <>
    
  752.         <div>Pending: false</div>
    
  753.         <div>Items in cart: 1</div>
    
  754.         <ul>
    
  755.           <li>Item A</li>
    
  756.         </ul>
    
  757.       </>,
    
  758.     );
    
  759. 
    
  760.     // The cart size is incremented even though B hasn't been added yet.
    
  761.     await act(() => addItemToCart('B'));
    
  762.     assertLog(['Pending: true', 'Items in cart: 2', 'Item A']);
    
  763.     expect(root).toMatchRenderedOutput(
    
  764.       <>
    
  765.         <div>Pending: true</div>
    
  766.         <div>Items in cart: 2</div>
    
  767.         <ul>
    
  768.           <li>Item A</li>
    
  769.         </ul>
    
  770.       </>,
    
  771.     );
    
  772. 
    
  773.     // While B is still pending, another item gets added to the cart
    
  774.     // out-of-band.
    
  775.     serverCart = [...serverCart, 'C'];
    
  776.     // NOTE: This is a synchronous update only because we don't yet support
    
  777.     // parallel transitions; all transitions are entangled together. Once we add
    
  778.     // support for parallel transitions, we can update this test.
    
  779.     ReactNoop.flushSync(() => root.render(<App cart={serverCart} />));
    
  780.     assertLog([
    
  781.       'Pending: true',
    
  782.       // Note that the optimistic cart size is still correct, because the
    
  783.       // pending update was rebased on top new value.
    
  784.       'Items in cart: 3',
    
  785.       'Item A',
    
  786.       'Item C',
    
  787.     ]);
    
  788.     expect(root).toMatchRenderedOutput(
    
  789.       <>
    
  790.         <div>Pending: true</div>
    
  791.         <div>Items in cart: 3</div>
    
  792.         <ul>
    
  793.           <li>Item A</li>
    
  794.           <li>Item C</li>
    
  795.         </ul>
    
  796.       </>,
    
  797.     );
    
  798. 
    
  799.     // Finish loading B. The optimistic state is reverted.
    
  800.     await act(() => resolveText('Adding item B'));
    
  801.     assertLog([
    
  802.       'Pending: false',
    
  803.       'Items in cart: 3',
    
  804.       'Item A',
    
  805.       'Item C',
    
  806.       'Item B',
    
  807.     ]);
    
  808.     expect(root).toMatchRenderedOutput(
    
  809.       <>
    
  810.         <div>Pending: false</div>
    
  811.         <div>Items in cart: 3</div>
    
  812.         <ul>
    
  813.           <li>Item A</li>
    
  814.           <li>Item C</li>
    
  815.           <li>Item B</li>
    
  816.         </ul>
    
  817.       </>,
    
  818.     );
    
  819.   });
    
  820. 
    
  821.   // @gate enableAsyncActions
    
  822.   test('regression: useOptimistic during setState-in-render', async () => {
    
  823.     // This is a regression test for a very specific case where useOptimistic is
    
  824.     // the first hook in the component, it has a pending update, and a later
    
  825.     // hook schedules a local (setState-in-render) update. Don't sweat about
    
  826.     // deleting this test if the implementation details change.
    
  827. 
    
  828.     let setOptimisticState;
    
  829.     let startTransition;
    
  830.     function App() {
    
  831.       const [optimisticState, _setOptimisticState] = useOptimistic(0);
    
  832.       setOptimisticState = _setOptimisticState;
    
  833.       const [, _startTransition] = useTransition();
    
  834.       startTransition = _startTransition;
    
  835. 
    
  836.       const [derivedState, setDerivedState] = useState(0);
    
  837.       if (derivedState !== optimisticState) {
    
  838.         setDerivedState(optimisticState);
    
  839.       }
    
  840. 
    
  841.       return <Text text={optimisticState} />;
    
  842.     }
    
  843. 
    
  844.     const root = ReactNoop.createRoot();
    
  845.     await act(() => root.render(<App />));
    
  846.     assertLog([0]);
    
  847.     expect(root).toMatchRenderedOutput('0');
    
  848. 
    
  849.     await act(() => {
    
  850.       startTransition(async () => {
    
  851.         setOptimisticState(1);
    
  852.         await getText('Wait');
    
  853.       });
    
  854.     });
    
  855.     assertLog([1]);
    
  856.     expect(root).toMatchRenderedOutput('1');
    
  857.   });
    
  858. 
    
  859.   // @gate enableAsyncActions
    
  860.   test('useOptimistic accepts a custom reducer', async () => {
    
  861.     let serverCart = ['A'];
    
  862. 
    
  863.     async function submitNewItem(item) {
    
  864.       await getText('Adding item ' + item);
    
  865.       serverCart = [...serverCart, item];
    
  866.       React.startTransition(() => {
    
  867.         root.render(<App cart={serverCart} />);
    
  868.       });
    
  869.     }
    
  870. 
    
  871.     let addItemToCart;
    
  872.     function App({cart}) {
    
  873.       const [isPending, startTransition] = useTransition();
    
  874. 
    
  875.       const savedCartSize = cart.length;
    
  876.       const [optimisticCartSize, addToOptimisticCart] = useOptimistic(
    
  877.         savedCartSize,
    
  878.         (prevSize, newItem) => {
    
  879.           Scheduler.log('Increment optimistic cart size for ' + newItem);
    
  880.           return prevSize + 1;
    
  881.         },
    
  882.       );
    
  883. 
    
  884.       addItemToCart = item => {
    
  885.         startTransition(async () => {
    
  886.           addToOptimisticCart(item);
    
  887.           await submitNewItem(item);
    
  888.         });
    
  889.       };
    
  890. 
    
  891.       return (
    
  892.         <>
    
  893.           <div>
    
  894.             <Text text={'Pending: ' + isPending} />
    
  895.           </div>
    
  896.           <div>
    
  897.             <Text text={'Items in cart: ' + optimisticCartSize} />
    
  898.           </div>
    
  899.           <ul>
    
  900.             {cart.map(item => (
    
  901.               <li key={item}>
    
  902.                 <Text text={'Item ' + item} />
    
  903.               </li>
    
  904.             ))}
    
  905.           </ul>
    
  906.         </>
    
  907.       );
    
  908.     }
    
  909. 
    
  910.     // Initial render
    
  911.     const root = ReactNoop.createRoot();
    
  912.     await act(() => root.render(<App cart={serverCart} />));
    
  913.     assertLog(['Pending: false', 'Items in cart: 1', 'Item A']);
    
  914.     expect(root).toMatchRenderedOutput(
    
  915.       <>
    
  916.         <div>Pending: false</div>
    
  917.         <div>Items in cart: 1</div>
    
  918.         <ul>
    
  919.           <li>Item A</li>
    
  920.         </ul>
    
  921.       </>,
    
  922.     );
    
  923. 
    
  924.     // The cart size is incremented even though B hasn't been added yet.
    
  925.     await act(() => addItemToCart('B'));
    
  926.     assertLog([
    
  927.       'Increment optimistic cart size for B',
    
  928.       'Pending: true',
    
  929.       'Items in cart: 2',
    
  930.       'Item A',
    
  931.     ]);
    
  932.     expect(root).toMatchRenderedOutput(
    
  933.       <>
    
  934.         <div>Pending: true</div>
    
  935.         <div>Items in cart: 2</div>
    
  936.         <ul>
    
  937.           <li>Item A</li>
    
  938.         </ul>
    
  939.       </>,
    
  940.     );
    
  941. 
    
  942.     // While B is still pending, another item gets added to the cart
    
  943.     // out-of-band.
    
  944.     serverCart = [...serverCart, 'C'];
    
  945.     // NOTE: This is a synchronous update only because we don't yet support
    
  946.     // parallel transitions; all transitions are entangled together. Once we add
    
  947.     // support for parallel transitions, we can update this test.
    
  948.     ReactNoop.flushSync(() => root.render(<App cart={serverCart} />));
    
  949.     assertLog([
    
  950.       'Increment optimistic cart size for B',
    
  951.       'Pending: true',
    
  952.       // Note that the optimistic cart size is still correct, because the
    
  953.       // pending update was rebased on top new value.
    
  954.       'Items in cart: 3',
    
  955.       'Item A',
    
  956.       'Item C',
    
  957.     ]);
    
  958.     expect(root).toMatchRenderedOutput(
    
  959.       <>
    
  960.         <div>Pending: true</div>
    
  961.         <div>Items in cart: 3</div>
    
  962.         <ul>
    
  963.           <li>Item A</li>
    
  964.           <li>Item C</li>
    
  965.         </ul>
    
  966.       </>,
    
  967.     );
    
  968. 
    
  969.     // Finish loading B. The optimistic state is reverted.
    
  970.     await act(() => resolveText('Adding item B'));
    
  971.     assertLog([
    
  972.       'Pending: false',
    
  973.       'Items in cart: 3',
    
  974.       'Item A',
    
  975.       'Item C',
    
  976.       'Item B',
    
  977.     ]);
    
  978.     expect(root).toMatchRenderedOutput(
    
  979.       <>
    
  980.         <div>Pending: false</div>
    
  981.         <div>Items in cart: 3</div>
    
  982.         <ul>
    
  983.           <li>Item A</li>
    
  984.           <li>Item C</li>
    
  985.           <li>Item B</li>
    
  986.         </ul>
    
  987.       </>,
    
  988.     );
    
  989.   });
    
  990. 
    
  991.   // @gate enableAsyncActions
    
  992.   test('useOptimistic rebases if the passthrough is updated during a render phase update', async () => {
    
  993.     // This is kind of an esoteric case where it's hard to come up with a
    
  994.     // realistic real-world scenario but it should still work.
    
  995.     let increment;
    
  996.     let setCount;
    
  997.     function App() {
    
  998.       const [isPending, startTransition] = useTransition(2);
    
  999.       const [count, _setCount] = useState(0);
    
  1000.       setCount = _setCount;
    
  1001. 
    
  1002.       const [optimisticCount, setOptimisticCount] = useOptimistic(
    
  1003.         count,
    
  1004.         prev => {
    
  1005.           Scheduler.log('Increment optimistic count');
    
  1006.           return prev + 1;
    
  1007.         },
    
  1008.       );
    
  1009. 
    
  1010.       if (count === 1) {
    
  1011.         Scheduler.log('Render phase update count from 1 to 2');
    
  1012.         setCount(2);
    
  1013.       }
    
  1014. 
    
  1015.       increment = () =>
    
  1016.         startTransition(async () => {
    
  1017.           setOptimisticCount(n => n + 1);
    
  1018.           await getText('Wait to increment');
    
  1019.           React.startTransition(() => setCount(n => n + 1));
    
  1020.         });
    
  1021. 
    
  1022.       return (
    
  1023.         <>
    
  1024.           <div>
    
  1025.             <Text text={'Count: ' + count} />
    
  1026.           </div>
    
  1027.           {isPending ? (
    
  1028.             <div>
    
  1029.               <Text text={'Optimistic count: ' + optimisticCount} />
    
  1030.             </div>
    
  1031.           ) : null}
    
  1032.         </>
    
  1033.       );
    
  1034.     }
    
  1035. 
    
  1036.     const root = ReactNoop.createRoot();
    
  1037.     await act(() => root.render(<App />));
    
  1038.     assertLog(['Count: 0']);
    
  1039.     expect(root).toMatchRenderedOutput(<div>Count: 0</div>);
    
  1040. 
    
  1041.     await act(() => increment());
    
  1042.     assertLog([
    
  1043.       'Increment optimistic count',
    
  1044.       'Count: 0',
    
  1045.       'Optimistic count: 1',
    
  1046.     ]);
    
  1047.     expect(root).toMatchRenderedOutput(
    
  1048.       <>
    
  1049.         <div>Count: 0</div>
    
  1050.         <div>Optimistic count: 1</div>
    
  1051.       </>,
    
  1052.     );
    
  1053. 
    
  1054.     await act(() => setCount(1));
    
  1055.     assertLog([
    
  1056.       'Increment optimistic count',
    
  1057.       'Render phase update count from 1 to 2',
    
  1058.       // The optimistic update is rebased on top of the new passthrough value.
    
  1059.       'Increment optimistic count',
    
  1060.       'Count: 2',
    
  1061.       'Optimistic count: 3',
    
  1062.     ]);
    
  1063.     expect(root).toMatchRenderedOutput(
    
  1064.       <>
    
  1065.         <div>Count: 2</div>
    
  1066.         <div>Optimistic count: 3</div>
    
  1067.       </>,
    
  1068.     );
    
  1069. 
    
  1070.     // Finish the action
    
  1071.     await act(() => resolveText('Wait to increment'));
    
  1072.     assertLog(['Count: 3']);
    
  1073.     expect(root).toMatchRenderedOutput(<div>Count: 3</div>);
    
  1074.   });
    
  1075. 
    
  1076.   // @gate enableAsyncActions
    
  1077.   test('useOptimistic rebases if the passthrough is updated during a render phase update (initial mount)', async () => {
    
  1078.     // This is kind of an esoteric case where it's hard to come up with a
    
  1079.     // realistic real-world scenario but it should still work.
    
  1080.     function App() {
    
  1081.       const [count, setCount] = useState(0);
    
  1082.       const [optimisticCount] = useOptimistic(count);
    
  1083. 
    
  1084.       if (count === 0) {
    
  1085.         Scheduler.log('Render phase update count from 1 to 2');
    
  1086.         setCount(1);
    
  1087.       }
    
  1088. 
    
  1089.       return (
    
  1090.         <>
    
  1091.           <div>
    
  1092.             <Text text={'Count: ' + count} />
    
  1093.           </div>
    
  1094.           <div>
    
  1095.             <Text text={'Optimistic count: ' + optimisticCount} />
    
  1096.           </div>
    
  1097.         </>
    
  1098.       );
    
  1099.     }
    
  1100. 
    
  1101.     const root = ReactNoop.createRoot();
    
  1102.     await act(() => root.render(<App />));
    
  1103.     assertLog([
    
  1104.       'Render phase update count from 1 to 2',
    
  1105.       'Count: 1',
    
  1106.       'Optimistic count: 1',
    
  1107.     ]);
    
  1108.     expect(root).toMatchRenderedOutput(
    
  1109.       <>
    
  1110.         <div>Count: 1</div>
    
  1111.         <div>Optimistic count: 1</div>
    
  1112.       </>,
    
  1113.     );
    
  1114.   });
    
  1115. 
    
  1116.   // @gate enableAsyncActions
    
  1117.   test('useOptimistic can update repeatedly in the same async action', async () => {
    
  1118.     let startTransition;
    
  1119.     let setLoadingProgress;
    
  1120.     let setText;
    
  1121.     function App() {
    
  1122.       const [, _startTransition] = useTransition();
    
  1123.       const [text, _setText] = useState('A');
    
  1124.       const [loadingProgress, _setLoadingProgress] = useOptimistic(0);
    
  1125.       startTransition = _startTransition;
    
  1126.       setText = _setText;
    
  1127.       setLoadingProgress = _setLoadingProgress;
    
  1128. 
    
  1129.       return (
    
  1130.         <>
    
  1131.           {loadingProgress !== 0 ? (
    
  1132.             <div key="progress">
    
  1133.               <Text text={`Loading... (${loadingProgress})`} />
    
  1134.             </div>
    
  1135.           ) : null}
    
  1136.           <div key="real">
    
  1137.             <Text text={text} />
    
  1138.           </div>
    
  1139.         </>
    
  1140.       );
    
  1141.     }
    
  1142. 
    
  1143.     // Initial render
    
  1144.     const root = ReactNoop.createRoot();
    
  1145.     await act(() => root.render(<App />));
    
  1146.     assertLog(['A']);
    
  1147.     expect(root).toMatchRenderedOutput(<div>A</div>);
    
  1148. 
    
  1149.     await act(async () => {
    
  1150.       startTransition(async () => {
    
  1151.         setLoadingProgress('25%');
    
  1152.         await getText('Wait 1');
    
  1153.         setLoadingProgress('75%');
    
  1154.         await getText('Wait 2');
    
  1155.         startTransition(() => setText('B'));
    
  1156.       });
    
  1157.     });
    
  1158.     assertLog(['Loading... (25%)', 'A']);
    
  1159.     expect(root).toMatchRenderedOutput(
    
  1160.       <>
    
  1161.         <div>Loading... (25%)</div>
    
  1162.         <div>A</div>
    
  1163.       </>,
    
  1164.     );
    
  1165. 
    
  1166.     await act(() => resolveText('Wait 1'));
    
  1167.     assertLog(['Loading... (75%)', 'A']);
    
  1168.     expect(root).toMatchRenderedOutput(
    
  1169.       <>
    
  1170.         <div>Loading... (75%)</div>
    
  1171.         <div>A</div>
    
  1172.       </>,
    
  1173.     );
    
  1174. 
    
  1175.     await act(() => resolveText('Wait 2'));
    
  1176.     assertLog(['B']);
    
  1177.     expect(root).toMatchRenderedOutput(<div>B</div>);
    
  1178.   });
    
  1179. 
    
  1180.   // @gate enableAsyncActions
    
  1181.   test('useOptimistic warns if outside of a transition', async () => {
    
  1182.     let startTransition;
    
  1183.     let setLoadingProgress;
    
  1184.     let setText;
    
  1185.     function App() {
    
  1186.       const [, _startTransition] = useTransition();
    
  1187.       const [text, _setText] = useState('A');
    
  1188.       const [loadingProgress, _setLoadingProgress] = useOptimistic(0);
    
  1189.       startTransition = _startTransition;
    
  1190.       setText = _setText;
    
  1191.       setLoadingProgress = _setLoadingProgress;
    
  1192. 
    
  1193.       return (
    
  1194.         <>
    
  1195.           {loadingProgress !== 0 ? (
    
  1196.             <div key="progress">
    
  1197.               <Text text={`Loading... (${loadingProgress})`} />
    
  1198.             </div>
    
  1199.           ) : null}
    
  1200.           <div key="real">
    
  1201.             <Text text={text} />
    
  1202.           </div>
    
  1203.         </>
    
  1204.       );
    
  1205.     }
    
  1206. 
    
  1207.     // Initial render
    
  1208.     const root = ReactNoop.createRoot();
    
  1209.     await act(() => root.render(<App />));
    
  1210.     assertLog(['A']);
    
  1211.     expect(root).toMatchRenderedOutput(<div>A</div>);
    
  1212. 
    
  1213.     await expect(async () => {
    
  1214.       await act(() => {
    
  1215.         setLoadingProgress('25%');
    
  1216.         startTransition(() => setText('B'));
    
  1217.       });
    
  1218.     }).toErrorDev(
    
  1219.       'An optimistic state update occurred outside a transition or ' +
    
  1220.         'action. To fix, move the update to an action, or wrap ' +
    
  1221.         'with startTransition.',
    
  1222.       {withoutStack: true},
    
  1223.     );
    
  1224.     assertLog(['Loading... (25%)', 'A', 'B']);
    
  1225.     expect(root).toMatchRenderedOutput(<div>B</div>);
    
  1226.   });
    
  1227. });