1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let React;
    
  13. let ReactDOM;
    
  14. 
    
  15. let TestComponent;
    
  16. 
    
  17. describe('ReactCompositeComponent-state', () => {
    
  18.   beforeEach(() => {
    
  19.     React = require('react');
    
  20.     ReactDOM = require('react-dom');
    
  21. 
    
  22.     TestComponent = class extends React.Component {
    
  23.       constructor(props) {
    
  24.         super(props);
    
  25.         this.peekAtState('getInitialState', undefined, props);
    
  26.         this.state = {color: 'red'};
    
  27.       }
    
  28. 
    
  29.       peekAtState = (from, state = this.state, props = this.props) => {
    
  30.         props.stateListener(from, state && state.color);
    
  31.       };
    
  32. 
    
  33.       peekAtCallback = from => {
    
  34.         return () => this.peekAtState(from);
    
  35.       };
    
  36. 
    
  37.       setFavoriteColor(nextColor) {
    
  38.         this.setState(
    
  39.           {color: nextColor},
    
  40.           this.peekAtCallback('setFavoriteColor'),
    
  41.         );
    
  42.       }
    
  43. 
    
  44.       render() {
    
  45.         this.peekAtState('render');
    
  46.         return <div>{this.state.color}</div>;
    
  47.       }
    
  48. 
    
  49.       UNSAFE_componentWillMount() {
    
  50.         this.peekAtState('componentWillMount-start');
    
  51.         this.setState(function (state) {
    
  52.           this.peekAtState('before-setState-sunrise', state);
    
  53.         });
    
  54.         this.setState(
    
  55.           {color: 'sunrise'},
    
  56.           this.peekAtCallback('setState-sunrise'),
    
  57.         );
    
  58.         this.setState(function (state) {
    
  59.           this.peekAtState('after-setState-sunrise', state);
    
  60.         });
    
  61.         this.peekAtState('componentWillMount-after-sunrise');
    
  62.         this.setState(
    
  63.           {color: 'orange'},
    
  64.           this.peekAtCallback('setState-orange'),
    
  65.         );
    
  66.         this.setState(function (state) {
    
  67.           this.peekAtState('after-setState-orange', state);
    
  68.         });
    
  69.         this.peekAtState('componentWillMount-end');
    
  70.       }
    
  71. 
    
  72.       componentDidMount() {
    
  73.         this.peekAtState('componentDidMount-start');
    
  74.         this.setState(
    
  75.           {color: 'yellow'},
    
  76.           this.peekAtCallback('setState-yellow'),
    
  77.         );
    
  78.         this.peekAtState('componentDidMount-end');
    
  79.       }
    
  80. 
    
  81.       UNSAFE_componentWillReceiveProps(newProps) {
    
  82.         this.peekAtState('componentWillReceiveProps-start');
    
  83.         if (newProps.nextColor) {
    
  84.           this.setState(function (state) {
    
  85.             this.peekAtState('before-setState-receiveProps', state);
    
  86.             return {color: newProps.nextColor};
    
  87.           });
    
  88.           // No longer a public API, but we can test that it works internally by
    
  89.           // reaching into the updater.
    
  90.           this.updater.enqueueReplaceState(this, {color: undefined});
    
  91.           this.setState(function (state) {
    
  92.             this.peekAtState('before-setState-again-receiveProps', state);
    
  93.             return {color: newProps.nextColor};
    
  94.           }, this.peekAtCallback('setState-receiveProps'));
    
  95.           this.setState(function (state) {
    
  96.             this.peekAtState('after-setState-receiveProps', state);
    
  97.           });
    
  98.         }
    
  99.         this.peekAtState('componentWillReceiveProps-end');
    
  100.       }
    
  101. 
    
  102.       shouldComponentUpdate(nextProps, nextState) {
    
  103.         this.peekAtState('shouldComponentUpdate-currentState');
    
  104.         this.peekAtState('shouldComponentUpdate-nextState', nextState);
    
  105.         return true;
    
  106.       }
    
  107. 
    
  108.       UNSAFE_componentWillUpdate(nextProps, nextState) {
    
  109.         this.peekAtState('componentWillUpdate-currentState');
    
  110.         this.peekAtState('componentWillUpdate-nextState', nextState);
    
  111.       }
    
  112. 
    
  113.       componentDidUpdate(prevProps, prevState) {
    
  114.         this.peekAtState('componentDidUpdate-currentState');
    
  115.         this.peekAtState('componentDidUpdate-prevState', prevState);
    
  116.       }
    
  117. 
    
  118.       componentWillUnmount() {
    
  119.         this.peekAtState('componentWillUnmount');
    
  120.       }
    
  121.     };
    
  122.   });
    
  123. 
    
  124.   it('should support setting state', () => {
    
  125.     const container = document.createElement('div');
    
  126.     document.body.appendChild(container);
    
  127. 
    
  128.     const stateListener = jest.fn();
    
  129.     const instance = ReactDOM.render(
    
  130.       <TestComponent stateListener={stateListener} />,
    
  131.       container,
    
  132.       function peekAtInitialCallback() {
    
  133.         this.peekAtState('initial-callback');
    
  134.       },
    
  135.     );
    
  136.     ReactDOM.render(
    
  137.       <TestComponent stateListener={stateListener} nextColor="green" />,
    
  138.       container,
    
  139.       instance.peekAtCallback('setProps'),
    
  140.     );
    
  141.     instance.setFavoriteColor('blue');
    
  142.     instance.forceUpdate(instance.peekAtCallback('forceUpdate'));
    
  143. 
    
  144.     ReactDOM.unmountComponentAtNode(container);
    
  145. 
    
  146.     const expected = [
    
  147.       // there is no state when getInitialState() is called
    
  148.       ['getInitialState', null],
    
  149.       ['componentWillMount-start', 'red'],
    
  150.       // setState()'s only enqueue pending states.
    
  151.       ['componentWillMount-after-sunrise', 'red'],
    
  152.       ['componentWillMount-end', 'red'],
    
  153.       // pending state queue is processed
    
  154.       ['before-setState-sunrise', 'red'],
    
  155.       ['after-setState-sunrise', 'sunrise'],
    
  156.       ['after-setState-orange', 'orange'],
    
  157.       // pending state has been applied
    
  158.       ['render', 'orange'],
    
  159.       ['componentDidMount-start', 'orange'],
    
  160.       // setState-sunrise and setState-orange should be called here,
    
  161.       // after the bug in #1740
    
  162.       // componentDidMount() called setState({color:'yellow'}), which is async.
    
  163.       // The update doesn't happen until the next flush.
    
  164.       ['componentDidMount-end', 'orange'],
    
  165.       ['setState-sunrise', 'orange'],
    
  166.       ['setState-orange', 'orange'],
    
  167.       ['initial-callback', 'orange'],
    
  168.       ['shouldComponentUpdate-currentState', 'orange'],
    
  169.       ['shouldComponentUpdate-nextState', 'yellow'],
    
  170.       ['componentWillUpdate-currentState', 'orange'],
    
  171.       ['componentWillUpdate-nextState', 'yellow'],
    
  172.       ['render', 'yellow'],
    
  173.       ['componentDidUpdate-currentState', 'yellow'],
    
  174.       ['componentDidUpdate-prevState', 'orange'],
    
  175.       ['setState-yellow', 'yellow'],
    
  176.       ['componentWillReceiveProps-start', 'yellow'],
    
  177.       // setState({color:'green'}) only enqueues a pending state.
    
  178.       ['componentWillReceiveProps-end', 'yellow'],
    
  179.       // pending state queue is processed
    
  180.       // We keep updates in the queue to support
    
  181.       // replaceState(prevState => newState).
    
  182.       ['before-setState-receiveProps', 'yellow'],
    
  183.       ['before-setState-again-receiveProps', undefined],
    
  184.       ['after-setState-receiveProps', 'green'],
    
  185.       ['shouldComponentUpdate-currentState', 'yellow'],
    
  186.       ['shouldComponentUpdate-nextState', 'green'],
    
  187.       ['componentWillUpdate-currentState', 'yellow'],
    
  188.       ['componentWillUpdate-nextState', 'green'],
    
  189.       ['render', 'green'],
    
  190.       ['componentDidUpdate-currentState', 'green'],
    
  191.       ['componentDidUpdate-prevState', 'yellow'],
    
  192.       ['setState-receiveProps', 'green'],
    
  193.       ['setProps', 'green'],
    
  194.       // setFavoriteColor('blue')
    
  195.       ['shouldComponentUpdate-currentState', 'green'],
    
  196.       ['shouldComponentUpdate-nextState', 'blue'],
    
  197.       ['componentWillUpdate-currentState', 'green'],
    
  198.       ['componentWillUpdate-nextState', 'blue'],
    
  199.       ['render', 'blue'],
    
  200.       ['componentDidUpdate-currentState', 'blue'],
    
  201.       ['componentDidUpdate-prevState', 'green'],
    
  202.       ['setFavoriteColor', 'blue'],
    
  203.       // forceUpdate()
    
  204.       ['componentWillUpdate-currentState', 'blue'],
    
  205.       ['componentWillUpdate-nextState', 'blue'],
    
  206.       ['render', 'blue'],
    
  207.       ['componentDidUpdate-currentState', 'blue'],
    
  208.       ['componentDidUpdate-prevState', 'blue'],
    
  209.       ['forceUpdate', 'blue'],
    
  210.       // unmountComponent()
    
  211.       // state is available within `componentWillUnmount()`
    
  212.       ['componentWillUnmount', 'blue'],
    
  213.     ];
    
  214. 
    
  215.     expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n'));
    
  216.   });
    
  217. 
    
  218.   it('should call componentDidUpdate of children first', () => {
    
  219.     const container = document.createElement('div');
    
  220. 
    
  221.     let ops = [];
    
  222. 
    
  223.     let child = null;
    
  224.     let parent = null;
    
  225. 
    
  226.     class Child extends React.Component {
    
  227.       state = {bar: false};
    
  228.       componentDidMount() {
    
  229.         child = this;
    
  230.       }
    
  231.       componentDidUpdate() {
    
  232.         ops.push('child did update');
    
  233.       }
    
  234.       render() {
    
  235.         return <div />;
    
  236.       }
    
  237.     }
    
  238. 
    
  239.     let shouldUpdate = true;
    
  240. 
    
  241.     class Intermediate extends React.Component {
    
  242.       shouldComponentUpdate() {
    
  243.         return shouldUpdate;
    
  244.       }
    
  245.       render() {
    
  246.         return <Child />;
    
  247.       }
    
  248.     }
    
  249. 
    
  250.     class Parent extends React.Component {
    
  251.       state = {foo: false};
    
  252.       componentDidMount() {
    
  253.         parent = this;
    
  254.       }
    
  255.       componentDidUpdate() {
    
  256.         ops.push('parent did update');
    
  257.       }
    
  258.       render() {
    
  259.         return <Intermediate />;
    
  260.       }
    
  261.     }
    
  262. 
    
  263.     ReactDOM.render(<Parent />, container);
    
  264. 
    
  265.     ReactDOM.unstable_batchedUpdates(() => {
    
  266.       parent.setState({foo: true});
    
  267.       child.setState({bar: true});
    
  268.     });
    
  269.     // When we render changes top-down in a batch, children's componentDidUpdate
    
  270.     // happens before the parent.
    
  271.     expect(ops).toEqual(['child did update', 'parent did update']);
    
  272. 
    
  273.     shouldUpdate = false;
    
  274. 
    
  275.     ops = [];
    
  276. 
    
  277.     ReactDOM.unstable_batchedUpdates(() => {
    
  278.       parent.setState({foo: false});
    
  279.       child.setState({bar: false});
    
  280.     });
    
  281.     // We expect the same thing to happen if we bail out in the middle.
    
  282.     expect(ops).toEqual(['child did update', 'parent did update']);
    
  283.   });
    
  284. 
    
  285.   it('should batch unmounts', () => {
    
  286.     class Inner extends React.Component {
    
  287.       render() {
    
  288.         return <div />;
    
  289.       }
    
  290. 
    
  291.       componentWillUnmount() {
    
  292.         // This should get silently ignored (maybe with a warning), but it
    
  293.         // shouldn't break React.
    
  294.         outer.setState({showInner: false});
    
  295.       }
    
  296.     }
    
  297. 
    
  298.     class Outer extends React.Component {
    
  299.       state = {showInner: true};
    
  300. 
    
  301.       render() {
    
  302.         return <div>{this.state.showInner && <Inner />}</div>;
    
  303.       }
    
  304.     }
    
  305. 
    
  306.     const container = document.createElement('div');
    
  307.     const outer = ReactDOM.render(<Outer />, container);
    
  308.     expect(() => {
    
  309.       ReactDOM.unmountComponentAtNode(container);
    
  310.     }).not.toThrow();
    
  311.   });
    
  312. 
    
  313.   it('should update state when called from child cWRP', function () {
    
  314.     const log = [];
    
  315.     class Parent extends React.Component {
    
  316.       state = {value: 'one'};
    
  317.       render() {
    
  318.         log.push('parent render ' + this.state.value);
    
  319.         return <Child parent={this} value={this.state.value} />;
    
  320.       }
    
  321.     }
    
  322.     let updated = false;
    
  323.     class Child extends React.Component {
    
  324.       UNSAFE_componentWillReceiveProps() {
    
  325.         if (updated) {
    
  326.           return;
    
  327.         }
    
  328.         log.push('child componentWillReceiveProps ' + this.props.value);
    
  329.         this.props.parent.setState({value: 'two'});
    
  330.         log.push('child componentWillReceiveProps done ' + this.props.value);
    
  331.         updated = true;
    
  332.       }
    
  333.       render() {
    
  334.         log.push('child render ' + this.props.value);
    
  335.         return <div>{this.props.value}</div>;
    
  336.       }
    
  337.     }
    
  338.     const container = document.createElement('div');
    
  339.     ReactDOM.render(<Parent />, container);
    
  340.     ReactDOM.render(<Parent />, container);
    
  341.     expect(log).toEqual([
    
  342.       'parent render one',
    
  343.       'child render one',
    
  344.       'parent render one',
    
  345.       'child componentWillReceiveProps one',
    
  346.       'child componentWillReceiveProps done one',
    
  347.       'child render one',
    
  348.       'parent render two',
    
  349.       'child render two',
    
  350.     ]);
    
  351.   });
    
  352. 
    
  353.   it('should merge state when sCU returns false', function () {
    
  354.     const log = [];
    
  355.     class Test extends React.Component {
    
  356.       state = {a: 0};
    
  357.       render() {
    
  358.         return null;
    
  359.       }
    
  360.       shouldComponentUpdate(nextProps, nextState) {
    
  361.         log.push(
    
  362.           'scu from ' +
    
  363.             Object.keys(this.state) +
    
  364.             ' to ' +
    
  365.             Object.keys(nextState),
    
  366.         );
    
  367.         return false;
    
  368.       }
    
  369.     }
    
  370. 
    
  371.     const container = document.createElement('div');
    
  372.     const test = ReactDOM.render(<Test />, container);
    
  373.     test.setState({b: 0});
    
  374.     expect(log.length).toBe(1);
    
  375.     test.setState({c: 0});
    
  376.     expect(log.length).toBe(2);
    
  377.     expect(log).toEqual(['scu from a to a,b', 'scu from a,b to a,b,c']);
    
  378.   });
    
  379. 
    
  380.   it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', () => {
    
  381.     const ops = [];
    
  382.     class Test extends React.Component {
    
  383.       state = {step: 1, extra: true};
    
  384.       UNSAFE_componentWillReceiveProps() {
    
  385.         this.setState({step: 2}, () => {
    
  386.           // Tests that earlier setState callbacks are not dropped
    
  387.           ops.push(
    
  388.             `callback -- step: ${this.state.step}, extra: ${!!this.state
    
  389.               .extra}`,
    
  390.           );
    
  391.         });
    
  392.         // Treat like replaceState
    
  393.         this.state = {step: 3};
    
  394.       }
    
  395.       render() {
    
  396.         ops.push(
    
  397.           `render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
    
  398.         );
    
  399.         return null;
    
  400.       }
    
  401.     }
    
  402. 
    
  403.     // Mount
    
  404.     const container = document.createElement('div');
    
  405.     ReactDOM.render(<Test />, container);
    
  406.     // Update
    
  407.     expect(() => ReactDOM.render(<Test />, container)).toErrorDev(
    
  408.       'Warning: Test.componentWillReceiveProps(): Assigning directly to ' +
    
  409.         "this.state is deprecated (except inside a component's constructor). " +
    
  410.         'Use setState instead.',
    
  411.     );
    
  412. 
    
  413.     expect(ops).toEqual([
    
  414.       'render -- step: 1, extra: true',
    
  415.       'render -- step: 3, extra: false',
    
  416.       'callback -- step: 3, extra: false',
    
  417.     ]);
    
  418. 
    
  419.     // Check deduplication; (no additional warnings are expected)
    
  420.     ReactDOM.render(<Test />, container);
    
  421.   });
    
  422. 
    
  423.   it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
    
  424.     const ops = [];
    
  425.     class Test extends React.Component {
    
  426.       state = {step: 1, extra: true};
    
  427.       UNSAFE_componentWillMount() {
    
  428.         this.setState({step: 2}, () => {
    
  429.           // Tests that earlier setState callbacks are not dropped
    
  430.           ops.push(
    
  431.             `callback -- step: ${this.state.step}, extra: ${!!this.state
    
  432.               .extra}`,
    
  433.           );
    
  434.         });
    
  435.         // Treat like replaceState
    
  436.         this.state = {step: 3};
    
  437.       }
    
  438.       render() {
    
  439.         ops.push(
    
  440.           `render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
    
  441.         );
    
  442.         return null;
    
  443.       }
    
  444.     }
    
  445. 
    
  446.     // Mount
    
  447.     const container = document.createElement('div');
    
  448.     expect(() => ReactDOM.render(<Test />, container)).toErrorDev(
    
  449.       'Warning: Test.componentWillMount(): Assigning directly to ' +
    
  450.         "this.state is deprecated (except inside a component's constructor). " +
    
  451.         'Use setState instead.',
    
  452.     );
    
  453. 
    
  454.     expect(ops).toEqual([
    
  455.       'render -- step: 3, extra: false',
    
  456.       'callback -- step: 3, extra: false',
    
  457.     ]);
    
  458.   });
    
  459. 
    
  460.   if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
    
  461.     it('should support stateful module pattern components', () => {
    
  462.       function Child() {
    
  463.         return {
    
  464.           state: {
    
  465.             count: 123,
    
  466.           },
    
  467.           render() {
    
  468.             return <div>{`count:${this.state.count}`}</div>;
    
  469.           },
    
  470.         };
    
  471.       }
    
  472. 
    
  473.       const el = document.createElement('div');
    
  474.       expect(() => ReactDOM.render(<Child />, el)).toErrorDev(
    
  475.         'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
    
  476.           'Change Child to a class that extends React.Component instead. ' +
    
  477.           "If you can't use a class try assigning the prototype on the function as a workaround. " +
    
  478.           '`Child.prototype = React.Component.prototype`. ' +
    
  479.           "Don't use an arrow function since it cannot be called with `new` by React.",
    
  480.       );
    
  481. 
    
  482.       expect(el.textContent).toBe('count:123');
    
  483.     });
    
  484. 
    
  485.     it('should support getDerivedStateFromProps for module pattern components', () => {
    
  486.       function Child() {
    
  487.         return {
    
  488.           state: {
    
  489.             count: 1,
    
  490.           },
    
  491.           render() {
    
  492.             return <div>{`count:${this.state.count}`}</div>;
    
  493.           },
    
  494.         };
    
  495.       }
    
  496.       Child.getDerivedStateFromProps = (props, prevState) => {
    
  497.         return {
    
  498.           count: prevState.count + props.incrementBy,
    
  499.         };
    
  500.       };
    
  501. 
    
  502.       const el = document.createElement('div');
    
  503.       ReactDOM.render(<Child incrementBy={0} />, el);
    
  504.       expect(el.textContent).toBe('count:1');
    
  505. 
    
  506.       ReactDOM.render(<Child incrementBy={2} />, el);
    
  507.       expect(el.textContent).toBe('count:3');
    
  508. 
    
  509.       ReactDOM.render(<Child incrementBy={1} />, el);
    
  510.       expect(el.textContent).toBe('count:4');
    
  511.     });
    
  512.   }
    
  513. 
    
  514.   it('should support setState in componentWillUnmount', () => {
    
  515.     let subscription;
    
  516.     class A extends React.Component {
    
  517.       componentWillUnmount() {
    
  518.         subscription();
    
  519.       }
    
  520.       render() {
    
  521.         return 'A';
    
  522.       }
    
  523.     }
    
  524. 
    
  525.     class B extends React.Component {
    
  526.       state = {siblingUnmounted: false};
    
  527.       UNSAFE_componentWillMount() {
    
  528.         subscription = () => this.setState({siblingUnmounted: true});
    
  529.       }
    
  530.       render() {
    
  531.         return 'B' + (this.state.siblingUnmounted ? ' No Sibling' : '');
    
  532.       }
    
  533.     }
    
  534. 
    
  535.     const el = document.createElement('div');
    
  536.     ReactDOM.render(<A />, el);
    
  537.     expect(el.textContent).toBe('A');
    
  538. 
    
  539.     ReactDOM.render(<B />, el);
    
  540.     expect(el.textContent).toBe('B No Sibling');
    
  541.   });
    
  542. });