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. /* eslint-disable no-func-assign */
    
  12. 
    
  13. 'use strict';
    
  14. 
    
  15. import {useInsertionEffect} from 'react';
    
  16. 
    
  17. describe('useEffectEvent', () => {
    
  18.   let React;
    
  19.   let ReactNoop;
    
  20.   let Scheduler;
    
  21.   let act;
    
  22.   let createContext;
    
  23.   let useContext;
    
  24.   let useState;
    
  25.   let useEffectEvent;
    
  26.   let useEffect;
    
  27.   let useLayoutEffect;
    
  28.   let useMemo;
    
  29.   let waitForAll;
    
  30.   let assertLog;
    
  31.   let waitForThrow;
    
  32. 
    
  33.   beforeEach(() => {
    
  34.     React = require('react');
    
  35.     ReactNoop = require('react-noop-renderer');
    
  36.     Scheduler = require('scheduler');
    
  37. 
    
  38.     act = require('internal-test-utils').act;
    
  39.     createContext = React.createContext;
    
  40.     useContext = React.useContext;
    
  41.     useState = React.useState;
    
  42.     useEffectEvent = React.experimental_useEffectEvent;
    
  43.     useEffect = React.useEffect;
    
  44.     useLayoutEffect = React.useLayoutEffect;
    
  45.     useMemo = React.useMemo;
    
  46. 
    
  47.     const InternalTestUtils = require('internal-test-utils');
    
  48.     waitForAll = InternalTestUtils.waitForAll;
    
  49.     assertLog = InternalTestUtils.assertLog;
    
  50.     waitForThrow = InternalTestUtils.waitForThrow;
    
  51.   });
    
  52. 
    
  53.   function Text(props) {
    
  54.     Scheduler.log(props.text);
    
  55.     return <span prop={props.text} />;
    
  56.   }
    
  57. 
    
  58.   // @gate enableUseEffectEventHook
    
  59.   it('memoizes basic case correctly', async () => {
    
  60.     class IncrementButton extends React.PureComponent {
    
  61.       increment = () => {
    
  62.         this.props.onClick();
    
  63.       };
    
  64.       render() {
    
  65.         return <Text text="Increment" />;
    
  66.       }
    
  67.     }
    
  68. 
    
  69.     function Counter({incrementBy}) {
    
  70.       const [count, updateCount] = useState(0);
    
  71.       const onClick = useEffectEvent(() => updateCount(c => c + incrementBy));
    
  72. 
    
  73.       return (
    
  74.         <>
    
  75.           <IncrementButton onClick={() => onClick()} ref={button} />
    
  76.           <Text text={'Count: ' + count} />
    
  77.         </>
    
  78.       );
    
  79.     }
    
  80. 
    
  81.     const button = React.createRef(null);
    
  82.     ReactNoop.render(<Counter incrementBy={1} />);
    
  83.     await waitForAll(['Increment', 'Count: 0']);
    
  84.     expect(ReactNoop).toMatchRenderedOutput(
    
  85.       <>
    
  86.         <span prop="Increment" />
    
  87.         <span prop="Count: 0" />
    
  88.       </>,
    
  89.     );
    
  90. 
    
  91.     await act(() => button.current.increment());
    
  92.     assertLog(['Increment', 'Count: 1']);
    
  93.     expect(ReactNoop).toMatchRenderedOutput(
    
  94.       <>
    
  95.         <span prop="Increment" />
    
  96.         <span prop="Count: 1" />
    
  97.       </>,
    
  98.     );
    
  99. 
    
  100.     await act(() => button.current.increment());
    
  101.     assertLog([
    
  102.       'Increment',
    
  103.       // Event should use the updated callback function closed over the new value.
    
  104.       'Count: 2',
    
  105.     ]);
    
  106.     expect(ReactNoop).toMatchRenderedOutput(
    
  107.       <>
    
  108.         <span prop="Increment" />
    
  109.         <span prop="Count: 2" />
    
  110.       </>,
    
  111.     );
    
  112. 
    
  113.     // Increase the increment prop amount
    
  114.     ReactNoop.render(<Counter incrementBy={10} />);
    
  115.     await waitForAll(['Increment', 'Count: 2']);
    
  116.     expect(ReactNoop).toMatchRenderedOutput(
    
  117.       <>
    
  118.         <span prop="Increment" />
    
  119.         <span prop="Count: 2" />
    
  120.       </>,
    
  121.     );
    
  122. 
    
  123.     // Event uses the new prop
    
  124.     await act(() => button.current.increment());
    
  125.     assertLog(['Increment', 'Count: 12']);
    
  126.     expect(ReactNoop).toMatchRenderedOutput(
    
  127.       <>
    
  128.         <span prop="Increment" />
    
  129.         <span prop="Count: 12" />
    
  130.       </>,
    
  131.     );
    
  132.   });
    
  133. 
    
  134.   // @gate enableUseEffectEventHook
    
  135.   it('can be defined more than once', async () => {
    
  136.     class IncrementButton extends React.PureComponent {
    
  137.       increment = () => {
    
  138.         this.props.onClick();
    
  139.       };
    
  140.       multiply = () => {
    
  141.         this.props.onMouseEnter();
    
  142.       };
    
  143.       render() {
    
  144.         return <Text text="Increment" />;
    
  145.       }
    
  146.     }
    
  147. 
    
  148.     function Counter({incrementBy}) {
    
  149.       const [count, updateCount] = useState(0);
    
  150.       const onClick = useEffectEvent(() => updateCount(c => c + incrementBy));
    
  151.       const onMouseEnter = useEffectEvent(() => {
    
  152.         updateCount(c => c * incrementBy);
    
  153.       });
    
  154. 
    
  155.       return (
    
  156.         <>
    
  157.           <IncrementButton
    
  158.             onClick={() => onClick()}
    
  159.             onMouseEnter={() => onMouseEnter()}
    
  160.             ref={button}
    
  161.           />
    
  162.           <Text text={'Count: ' + count} />
    
  163.         </>
    
  164.       );
    
  165.     }
    
  166. 
    
  167.     const button = React.createRef(null);
    
  168.     ReactNoop.render(<Counter incrementBy={5} />);
    
  169.     await waitForAll(['Increment', 'Count: 0']);
    
  170.     expect(ReactNoop).toMatchRenderedOutput(
    
  171.       <>
    
  172.         <span prop="Increment" />
    
  173.         <span prop="Count: 0" />
    
  174.       </>,
    
  175.     );
    
  176. 
    
  177.     await act(() => button.current.increment());
    
  178.     assertLog(['Increment', 'Count: 5']);
    
  179.     expect(ReactNoop).toMatchRenderedOutput(
    
  180.       <>
    
  181.         <span prop="Increment" />
    
  182.         <span prop="Count: 5" />
    
  183.       </>,
    
  184.     );
    
  185. 
    
  186.     await act(() => button.current.multiply());
    
  187.     assertLog(['Increment', 'Count: 25']);
    
  188.     expect(ReactNoop).toMatchRenderedOutput(
    
  189.       <>
    
  190.         <span prop="Increment" />
    
  191.         <span prop="Count: 25" />
    
  192.       </>,
    
  193.     );
    
  194.   });
    
  195. 
    
  196.   // @gate enableUseEffectEventHook
    
  197.   it('does not preserve `this` in event functions', async () => {
    
  198.     class GreetButton extends React.PureComponent {
    
  199.       greet = () => {
    
  200.         this.props.onClick();
    
  201.       };
    
  202.       render() {
    
  203.         return <Text text={'Say ' + this.props.hello} />;
    
  204.       }
    
  205.     }
    
  206.     function Greeter({hello}) {
    
  207.       const person = {
    
  208.         toString() {
    
  209.           return 'Jane';
    
  210.         },
    
  211.         greet() {
    
  212.           return updateGreeting(this + ' says ' + hello);
    
  213.         },
    
  214.       };
    
  215.       const [greeting, updateGreeting] = useState('Seb says ' + hello);
    
  216.       const onClick = useEffectEvent(person.greet);
    
  217. 
    
  218.       return (
    
  219.         <>
    
  220.           <GreetButton hello={hello} onClick={() => onClick()} ref={button} />
    
  221.           <Text text={'Greeting: ' + greeting} />
    
  222.         </>
    
  223.       );
    
  224.     }
    
  225. 
    
  226.     const button = React.createRef(null);
    
  227.     ReactNoop.render(<Greeter hello={'hej'} />);
    
  228.     await waitForAll(['Say hej', 'Greeting: Seb says hej']);
    
  229.     expect(ReactNoop).toMatchRenderedOutput(
    
  230.       <>
    
  231.         <span prop="Say hej" />
    
  232.         <span prop="Greeting: Seb says hej" />
    
  233.       </>,
    
  234.     );
    
  235. 
    
  236.     await act(() => button.current.greet());
    
  237.     assertLog(['Say hej', 'Greeting: undefined says hej']);
    
  238.     expect(ReactNoop).toMatchRenderedOutput(
    
  239.       <>
    
  240.         <span prop="Say hej" />
    
  241.         <span prop="Greeting: undefined says hej" />
    
  242.       </>,
    
  243.     );
    
  244.   });
    
  245. 
    
  246.   // @gate enableUseEffectEventHook
    
  247.   it('throws when called in render', async () => {
    
  248.     class IncrementButton extends React.PureComponent {
    
  249.       increment = () => {
    
  250.         this.props.onClick();
    
  251.       };
    
  252. 
    
  253.       render() {
    
  254.         // Will throw.
    
  255.         this.props.onClick();
    
  256. 
    
  257.         return <Text text="Increment" />;
    
  258.       }
    
  259.     }
    
  260. 
    
  261.     function Counter({incrementBy}) {
    
  262.       const [count, updateCount] = useState(0);
    
  263.       const onClick = useEffectEvent(() => updateCount(c => c + incrementBy));
    
  264. 
    
  265.       return (
    
  266.         <>
    
  267.           <IncrementButton onClick={() => onClick()} />
    
  268.           <Text text={'Count: ' + count} />
    
  269.         </>
    
  270.       );
    
  271.     }
    
  272. 
    
  273.     ReactNoop.render(<Counter incrementBy={1} />);
    
  274.     await waitForThrow(
    
  275.       "A function wrapped in useEffectEvent can't be called during rendering.",
    
  276.     );
    
  277.     assertLog([]);
    
  278.   });
    
  279. 
    
  280.   // @gate enableUseEffectEventHook
    
  281.   it("useLayoutEffect shouldn't re-fire when event handlers change", async () => {
    
  282.     class IncrementButton extends React.PureComponent {
    
  283.       increment = () => {
    
  284.         this.props.onClick();
    
  285.       };
    
  286.       render() {
    
  287.         return <Text text="Increment" />;
    
  288.       }
    
  289.     }
    
  290. 
    
  291.     function Counter({incrementBy}) {
    
  292.       const [count, updateCount] = useState(0);
    
  293.       const increment = useEffectEvent(amount =>
    
  294.         updateCount(c => c + (amount || incrementBy)),
    
  295.       );
    
  296. 
    
  297.       useLayoutEffect(() => {
    
  298.         Scheduler.log('Effect: by ' + incrementBy * 2);
    
  299.         increment(incrementBy * 2);
    
  300.       }, [incrementBy]);
    
  301. 
    
  302.       return (
    
  303.         <>
    
  304.           <IncrementButton onClick={() => increment()} ref={button} />
    
  305.           <Text text={'Count: ' + count} />
    
  306.         </>
    
  307.       );
    
  308.     }
    
  309. 
    
  310.     const button = React.createRef(null);
    
  311.     ReactNoop.render(<Counter incrementBy={1} />);
    
  312.     assertLog([]);
    
  313.     await waitForAll([
    
  314.       'Increment',
    
  315.       'Count: 0',
    
  316.       'Effect: by 2',
    
  317.       'Increment',
    
  318.       'Count: 2',
    
  319.     ]);
    
  320.     expect(ReactNoop).toMatchRenderedOutput(
    
  321.       <>
    
  322.         <span prop="Increment" />
    
  323.         <span prop="Count: 2" />
    
  324.       </>,
    
  325.     );
    
  326. 
    
  327.     await act(() => button.current.increment());
    
  328.     assertLog([
    
  329.       'Increment',
    
  330.       // Effect should not re-run because the dependency hasn't changed.
    
  331.       'Count: 3',
    
  332.     ]);
    
  333.     expect(ReactNoop).toMatchRenderedOutput(
    
  334.       <>
    
  335.         <span prop="Increment" />
    
  336.         <span prop="Count: 3" />
    
  337.       </>,
    
  338.     );
    
  339. 
    
  340.     await act(() => button.current.increment());
    
  341.     assertLog([
    
  342.       'Increment',
    
  343.       // Event should use the updated callback function closed over the new value.
    
  344.       'Count: 4',
    
  345.     ]);
    
  346.     expect(ReactNoop).toMatchRenderedOutput(
    
  347.       <>
    
  348.         <span prop="Increment" />
    
  349.         <span prop="Count: 4" />
    
  350.       </>,
    
  351.     );
    
  352. 
    
  353.     // Increase the increment prop amount
    
  354.     ReactNoop.render(<Counter incrementBy={10} />);
    
  355.     await waitForAll([
    
  356.       'Increment',
    
  357.       'Count: 4',
    
  358.       'Effect: by 20',
    
  359.       'Increment',
    
  360.       'Count: 24',
    
  361.     ]);
    
  362.     expect(ReactNoop).toMatchRenderedOutput(
    
  363.       <>
    
  364.         <span prop="Increment" />
    
  365.         <span prop="Count: 24" />
    
  366.       </>,
    
  367.     );
    
  368. 
    
  369.     // Event uses the new prop
    
  370.     await act(() => button.current.increment());
    
  371.     assertLog(['Increment', 'Count: 34']);
    
  372.     expect(ReactNoop).toMatchRenderedOutput(
    
  373.       <>
    
  374.         <span prop="Increment" />
    
  375.         <span prop="Count: 34" />
    
  376.       </>,
    
  377.     );
    
  378.   });
    
  379. 
    
  380.   // @gate enableUseEffectEventHook
    
  381.   it("useEffect shouldn't re-fire when event handlers change", async () => {
    
  382.     class IncrementButton extends React.PureComponent {
    
  383.       increment = () => {
    
  384.         this.props.onClick();
    
  385.       };
    
  386.       render() {
    
  387.         return <Text text="Increment" />;
    
  388.       }
    
  389.     }
    
  390. 
    
  391.     function Counter({incrementBy}) {
    
  392.       const [count, updateCount] = useState(0);
    
  393.       const increment = useEffectEvent(amount =>
    
  394.         updateCount(c => c + (amount || incrementBy)),
    
  395.       );
    
  396. 
    
  397.       useEffect(() => {
    
  398.         Scheduler.log('Effect: by ' + incrementBy * 2);
    
  399.         increment(incrementBy * 2);
    
  400.       }, [incrementBy]);
    
  401. 
    
  402.       return (
    
  403.         <>
    
  404.           <IncrementButton onClick={() => increment()} ref={button} />
    
  405.           <Text text={'Count: ' + count} />
    
  406.         </>
    
  407.       );
    
  408.     }
    
  409. 
    
  410.     const button = React.createRef(null);
    
  411.     ReactNoop.render(<Counter incrementBy={1} />);
    
  412.     await waitForAll([
    
  413.       'Increment',
    
  414.       'Count: 0',
    
  415.       'Effect: by 2',
    
  416.       'Increment',
    
  417.       'Count: 2',
    
  418.     ]);
    
  419.     expect(ReactNoop).toMatchRenderedOutput(
    
  420.       <>
    
  421.         <span prop="Increment" />
    
  422.         <span prop="Count: 2" />
    
  423.       </>,
    
  424.     );
    
  425. 
    
  426.     await act(() => button.current.increment());
    
  427.     assertLog([
    
  428.       'Increment',
    
  429.       // Effect should not re-run because the dependency hasn't changed.
    
  430.       'Count: 3',
    
  431.     ]);
    
  432.     expect(ReactNoop).toMatchRenderedOutput(
    
  433.       <>
    
  434.         <span prop="Increment" />
    
  435.         <span prop="Count: 3" />
    
  436.       </>,
    
  437.     );
    
  438. 
    
  439.     await act(() => button.current.increment());
    
  440.     assertLog([
    
  441.       'Increment',
    
  442.       // Event should use the updated callback function closed over the new value.
    
  443.       'Count: 4',
    
  444.     ]);
    
  445.     expect(ReactNoop).toMatchRenderedOutput(
    
  446.       <>
    
  447.         <span prop="Increment" />
    
  448.         <span prop="Count: 4" />
    
  449.       </>,
    
  450.     );
    
  451. 
    
  452.     // Increase the increment prop amount
    
  453.     ReactNoop.render(<Counter incrementBy={10} />);
    
  454.     await waitForAll([
    
  455.       'Increment',
    
  456.       'Count: 4',
    
  457.       'Effect: by 20',
    
  458.       'Increment',
    
  459.       'Count: 24',
    
  460.     ]);
    
  461.     expect(ReactNoop).toMatchRenderedOutput(
    
  462.       <>
    
  463.         <span prop="Increment" />
    
  464.         <span prop="Count: 24" />
    
  465.       </>,
    
  466.     );
    
  467. 
    
  468.     // Event uses the new prop
    
  469.     await act(() => button.current.increment());
    
  470.     assertLog(['Increment', 'Count: 34']);
    
  471.     expect(ReactNoop).toMatchRenderedOutput(
    
  472.       <>
    
  473.         <span prop="Increment" />
    
  474.         <span prop="Count: 34" />
    
  475.       </>,
    
  476.     );
    
  477.   });
    
  478. 
    
  479.   // @gate enableUseEffectEventHook
    
  480.   it('is stable in a custom hook', async () => {
    
  481.     class IncrementButton extends React.PureComponent {
    
  482.       increment = () => {
    
  483.         this.props.onClick();
    
  484.       };
    
  485.       render() {
    
  486.         return <Text text="Increment" />;
    
  487.       }
    
  488.     }
    
  489. 
    
  490.     function useCount(incrementBy) {
    
  491.       const [count, updateCount] = useState(0);
    
  492.       const increment = useEffectEvent(amount =>
    
  493.         updateCount(c => c + (amount || incrementBy)),
    
  494.       );
    
  495. 
    
  496.       return [count, increment];
    
  497.     }
    
  498. 
    
  499.     function Counter({incrementBy}) {
    
  500.       const [count, increment] = useCount(incrementBy);
    
  501. 
    
  502.       useEffect(() => {
    
  503.         Scheduler.log('Effect: by ' + incrementBy * 2);
    
  504.         increment(incrementBy * 2);
    
  505.       }, [incrementBy]);
    
  506. 
    
  507.       return (
    
  508.         <>
    
  509.           <IncrementButton onClick={() => increment()} ref={button} />
    
  510.           <Text text={'Count: ' + count} />
    
  511.         </>
    
  512.       );
    
  513.     }
    
  514. 
    
  515.     const button = React.createRef(null);
    
  516.     ReactNoop.render(<Counter incrementBy={1} />);
    
  517.     await waitForAll([
    
  518.       'Increment',
    
  519.       'Count: 0',
    
  520.       'Effect: by 2',
    
  521.       'Increment',
    
  522.       'Count: 2',
    
  523.     ]);
    
  524.     expect(ReactNoop).toMatchRenderedOutput(
    
  525.       <>
    
  526.         <span prop="Increment" />
    
  527.         <span prop="Count: 2" />
    
  528.       </>,
    
  529.     );
    
  530. 
    
  531.     await act(() => button.current.increment());
    
  532.     assertLog([
    
  533.       'Increment',
    
  534.       // Effect should not re-run because the dependency hasn't changed.
    
  535.       'Count: 3',
    
  536.     ]);
    
  537.     expect(ReactNoop).toMatchRenderedOutput(
    
  538.       <>
    
  539.         <span prop="Increment" />
    
  540.         <span prop="Count: 3" />
    
  541.       </>,
    
  542.     );
    
  543. 
    
  544.     await act(() => button.current.increment());
    
  545.     assertLog([
    
  546.       'Increment',
    
  547.       // Event should use the updated callback function closed over the new value.
    
  548.       'Count: 4',
    
  549.     ]);
    
  550.     expect(ReactNoop).toMatchRenderedOutput(
    
  551.       <>
    
  552.         <span prop="Increment" />
    
  553.         <span prop="Count: 4" />
    
  554.       </>,
    
  555.     );
    
  556. 
    
  557.     // Increase the increment prop amount
    
  558.     ReactNoop.render(<Counter incrementBy={10} />);
    
  559.     await waitForAll([
    
  560.       'Increment',
    
  561.       'Count: 4',
    
  562.       'Effect: by 20',
    
  563.       'Increment',
    
  564.       'Count: 24',
    
  565.     ]);
    
  566.     expect(ReactNoop).toMatchRenderedOutput(
    
  567.       <>
    
  568.         <span prop="Increment" />
    
  569.         <span prop="Count: 24" />
    
  570.       </>,
    
  571.     );
    
  572. 
    
  573.     // Event uses the new prop
    
  574.     await act(() => button.current.increment());
    
  575.     assertLog(['Increment', 'Count: 34']);
    
  576.     expect(ReactNoop).toMatchRenderedOutput(
    
  577.       <>
    
  578.         <span prop="Increment" />
    
  579.         <span prop="Count: 34" />
    
  580.       </>,
    
  581.     );
    
  582.   });
    
  583. 
    
  584.   // @gate enableUseEffectEventHook
    
  585.   it('is mutated before all other effects', async () => {
    
  586.     function Counter({value}) {
    
  587.       useInsertionEffect(() => {
    
  588.         Scheduler.log('Effect value: ' + value);
    
  589.         increment();
    
  590.       }, [value]);
    
  591. 
    
  592.       // This is defined after the insertion effect, but it should
    
  593.       // update the event fn _before_ the insertion effect fires.
    
  594.       const increment = useEffectEvent(() => {
    
  595.         Scheduler.log('Event value: ' + value);
    
  596.       });
    
  597. 
    
  598.       return <></>;
    
  599.     }
    
  600. 
    
  601.     ReactNoop.render(<Counter value={1} />);
    
  602.     await waitForAll(['Effect value: 1', 'Event value: 1']);
    
  603. 
    
  604.     await act(() => ReactNoop.render(<Counter value={2} />));
    
  605.     assertLog(['Effect value: 2', 'Event value: 2']);
    
  606.   });
    
  607. 
    
  608.   // @gate enableUseEffectEventHook
    
  609.   it("doesn't provide a stable identity", async () => {
    
  610.     function Counter({shouldRender, value}) {
    
  611.       const onClick = useEffectEvent(() => {
    
  612.         Scheduler.log(
    
  613.           'onClick, shouldRender=' + shouldRender + ', value=' + value,
    
  614.         );
    
  615.       });
    
  616. 
    
  617.       // onClick doesn't have a stable function identity so this effect will fire on every render.
    
  618.       // In a real app useEffectEvent functions should *not* be passed as a dependency, this is for
    
  619.       // testing purposes only.
    
  620.       useEffect(() => {
    
  621.         onClick();
    
  622.       }, [onClick]);
    
  623. 
    
  624.       useEffect(() => {
    
  625.         onClick();
    
  626.       }, [shouldRender]);
    
  627. 
    
  628.       return <></>;
    
  629.     }
    
  630. 
    
  631.     ReactNoop.render(<Counter shouldRender={true} value={0} />);
    
  632.     await waitForAll([
    
  633.       'onClick, shouldRender=true, value=0',
    
  634.       'onClick, shouldRender=true, value=0',
    
  635.     ]);
    
  636. 
    
  637.     ReactNoop.render(<Counter shouldRender={true} value={1} />);
    
  638.     await waitForAll(['onClick, shouldRender=true, value=1']);
    
  639. 
    
  640.     ReactNoop.render(<Counter shouldRender={false} value={2} />);
    
  641.     await waitForAll([
    
  642.       'onClick, shouldRender=false, value=2',
    
  643.       'onClick, shouldRender=false, value=2',
    
  644.     ]);
    
  645.   });
    
  646. 
    
  647.   // @gate enableUseEffectEventHook
    
  648.   it('event handlers always see the latest committed value', async () => {
    
  649.     let committedEventHandler = null;
    
  650. 
    
  651.     function App({value}) {
    
  652.       const event = useEffectEvent(() => {
    
  653.         return 'Value seen by useEffectEvent: ' + value;
    
  654.       });
    
  655. 
    
  656.       // Set up an effect that registers the event handler with an external
    
  657.       // event system (e.g. addEventListener).
    
  658.       useEffect(
    
  659.         () => {
    
  660.           // Log when the effect fires. In the test below, we'll assert that this
    
  661.           // only happens during initial render, not during updates.
    
  662.           Scheduler.log('Commit new event handler');
    
  663.           committedEventHandler = event;
    
  664.           return () => {
    
  665.             committedEventHandler = null;
    
  666.           };
    
  667.         },
    
  668.         // Note that we've intentionally omitted the event from the dependency
    
  669.         // array. But it will still be able to see the latest `value`. This is the
    
  670.         // key feature of useEffectEvent that makes it different from a regular closure.
    
  671.         [],
    
  672.       );
    
  673.       return 'Latest rendered value ' + value;
    
  674.     }
    
  675. 
    
  676.     // Initial render
    
  677.     const root = ReactNoop.createRoot();
    
  678.     await act(() => {
    
  679.       root.render(<App value={1} />);
    
  680.     });
    
  681.     assertLog(['Commit new event handler']);
    
  682.     expect(root).toMatchRenderedOutput('Latest rendered value 1');
    
  683.     expect(committedEventHandler()).toBe('Value seen by useEffectEvent: 1');
    
  684. 
    
  685.     // Update
    
  686.     await act(() => {
    
  687.       root.render(<App value={2} />);
    
  688.     });
    
  689.     // No new event handler should be committed, because it was omitted from
    
  690.     // the dependency array.
    
  691.     assertLog([]);
    
  692.     // But the event handler should still be able to see the latest value.
    
  693.     expect(root).toMatchRenderedOutput('Latest rendered value 2');
    
  694.     expect(committedEventHandler()).toBe('Value seen by useEffectEvent: 2');
    
  695.   });
    
  696. 
    
  697.   // @gate enableUseEffectEventHook
    
  698.   it('integration: implements docs chat room example', async () => {
    
  699.     function createConnection() {
    
  700.       let connectedCallback;
    
  701.       let timeout;
    
  702.       return {
    
  703.         connect() {
    
  704.           timeout = setTimeout(() => {
    
  705.             if (connectedCallback) {
    
  706.               connectedCallback();
    
  707.             }
    
  708.           }, 100);
    
  709.         },
    
  710.         on(event, callback) {
    
  711.           if (connectedCallback) {
    
  712.             throw Error('Cannot add the handler twice.');
    
  713.           }
    
  714.           if (event !== 'connected') {
    
  715.             throw Error('Only "connected" event is supported.');
    
  716.           }
    
  717.           connectedCallback = callback;
    
  718.         },
    
  719.         disconnect() {
    
  720.           clearTimeout(timeout);
    
  721.         },
    
  722.       };
    
  723.     }
    
  724. 
    
  725.     function ChatRoom({roomId, theme}) {
    
  726.       const onConnected = useEffectEvent(() => {
    
  727.         Scheduler.log('Connected! theme: ' + theme);
    
  728.       });
    
  729. 
    
  730.       useEffect(() => {
    
  731.         const connection = createConnection(roomId);
    
  732.         connection.on('connected', () => {
    
  733.           onConnected();
    
  734.         });
    
  735.         connection.connect();
    
  736.         return () => connection.disconnect();
    
  737.       }, [roomId]);
    
  738. 
    
  739.       return <Text text={`Welcome to the ${roomId} room!`} />;
    
  740.     }
    
  741. 
    
  742.     await act(() =>
    
  743.       ReactNoop.render(<ChatRoom roomId="general" theme="light" />),
    
  744.     );
    
  745.     await act(() => jest.runAllTimers());
    
  746.     assertLog(['Welcome to the general room!', 'Connected! theme: light']);
    
  747.     expect(ReactNoop).toMatchRenderedOutput(
    
  748.       <span prop="Welcome to the general room!" />,
    
  749.     );
    
  750. 
    
  751.     // change roomId only
    
  752.     await act(() =>
    
  753.       ReactNoop.render(<ChatRoom roomId="music" theme="light" />),
    
  754.     );
    
  755.     await act(() => jest.runAllTimers());
    
  756.     assertLog([
    
  757.       'Welcome to the music room!',
    
  758.       // should trigger a reconnect
    
  759.       'Connected! theme: light',
    
  760.     ]);
    
  761. 
    
  762.     expect(ReactNoop).toMatchRenderedOutput(
    
  763.       <span prop="Welcome to the music room!" />,
    
  764.     );
    
  765. 
    
  766.     // change theme only
    
  767.     await act(() => ReactNoop.render(<ChatRoom roomId="music" theme="dark" />));
    
  768.     await act(() => jest.runAllTimers());
    
  769.     // should not trigger a reconnect
    
  770.     assertLog(['Welcome to the music room!']);
    
  771.     expect(ReactNoop).toMatchRenderedOutput(
    
  772.       <span prop="Welcome to the music room!" />,
    
  773.     );
    
  774. 
    
  775.     // change roomId only
    
  776.     await act(() =>
    
  777.       ReactNoop.render(<ChatRoom roomId="travel" theme="dark" />),
    
  778.     );
    
  779.     await act(() => jest.runAllTimers());
    
  780.     assertLog([
    
  781.       'Welcome to the travel room!',
    
  782.       // should trigger a reconnect
    
  783.       'Connected! theme: dark',
    
  784.     ]);
    
  785.     expect(ReactNoop).toMatchRenderedOutput(
    
  786.       <span prop="Welcome to the travel room!" />,
    
  787.     );
    
  788.   });
    
  789. 
    
  790.   // @gate enableUseEffectEventHook
    
  791.   it('integration: implements the docs logVisit example', async () => {
    
  792.     class AddToCartButton extends React.PureComponent {
    
  793.       addToCart = () => {
    
  794.         this.props.onClick();
    
  795.       };
    
  796.       render() {
    
  797.         return <Text text="Add to cart" />;
    
  798.       }
    
  799.     }
    
  800.     const ShoppingCartContext = createContext(null);
    
  801. 
    
  802.     function AppShell({children}) {
    
  803.       const [items, updateItems] = useState([]);
    
  804.       const value = useMemo(() => ({items, updateItems}), [items, updateItems]);
    
  805. 
    
  806.       return (
    
  807.         <ShoppingCartContext.Provider value={value}>
    
  808.           {children}
    
  809.         </ShoppingCartContext.Provider>
    
  810.       );
    
  811.     }
    
  812. 
    
  813.     function Page({url}) {
    
  814.       const {items, updateItems} = useContext(ShoppingCartContext);
    
  815.       const onClick = useEffectEvent(() => updateItems([...items, 1]));
    
  816.       const numberOfItems = items.length;
    
  817. 
    
  818.       const onVisit = useEffectEvent(visitedUrl => {
    
  819.         Scheduler.log(
    
  820.           'url: ' + visitedUrl + ', numberOfItems: ' + numberOfItems,
    
  821.         );
    
  822.       });
    
  823. 
    
  824.       useEffect(() => {
    
  825.         onVisit(url);
    
  826.       }, [url]);
    
  827. 
    
  828.       return (
    
  829.         <AddToCartButton
    
  830.           onClick={() => {
    
  831.             onClick();
    
  832.           }}
    
  833.           ref={button}
    
  834.         />
    
  835.       );
    
  836.     }
    
  837. 
    
  838.     const button = React.createRef(null);
    
  839.     await act(() =>
    
  840.       ReactNoop.render(
    
  841.         <AppShell>
    
  842.           <Page url="/shop/1" />
    
  843.         </AppShell>,
    
  844.       ),
    
  845.     );
    
  846.     assertLog(['Add to cart', 'url: /shop/1, numberOfItems: 0']);
    
  847.     await act(() => button.current.addToCart());
    
  848.     assertLog(['Add to cart']);
    
  849. 
    
  850.     await act(() =>
    
  851.       ReactNoop.render(
    
  852.         <AppShell>
    
  853.           <Page url="/shop/2" />
    
  854.         </AppShell>,
    
  855.       ),
    
  856.     );
    
  857.     assertLog(['Add to cart', 'url: /shop/2, numberOfItems: 1']);
    
  858.   });
    
  859. });