packages/render/dom/__tests__/ReactCompositeComponentState-test.js
'use strict';
let React;
let ReactDOM;
let TestComponent;
describe('ReactCompositeComponent-state', () => {
beforeEach(() => {
React = require('react');
ReactDOM = require('react-dom');
TestComponent = class extends React.Component {
constructor(props) {
super(props);
this.peekAtState('getInitialState', undefined, props);
this.state = { color: 'red' };
}
peekAtState = (from, state = this.state, props = this.props) => {
props.stateListener(from, state && state.color);
};
peekAtCallback = from => {
return () => this.peekAtState(from);
};
setFavoriteColor(nextColor) {
this.setState({ color: nextColor }, this.peekAtCallback('setFavoriteColor'));
}
render() {
this.peekAtState('render');
return <div> {this.state.color} </div>;
}
UNSAFE_componentWillMount() {
this.peekAtState('componentWillMount-start');
this.setState(function(state) {
this.peekAtState('before-setState-sunrise', state);
});
this.setState({ color: 'sunrise' }, this.peekAtCallback('setState-sunrise'));
this.setState(function(state) {
this.peekAtState('after-setState-sunrise', state);
});
this.peekAtState('componentWillMount-after-sunrise');
this.setState({ color: 'orange' }, this.peekAtCallback('setState-orange'));
this.setState(function(state) {
this.peekAtState('after-setState-orange', state);
});
this.peekAtState('componentWillMount-end');
}
componentDidMount() {
this.peekAtState('componentDidMount-start');
this.setState({ color: 'yellow' }, this.peekAtCallback('setState-yellow'));
this.peekAtState('componentDidMount-end');
}
UNSAFE_componentWillReceiveProps(newProps) {
this.peekAtState('componentWillReceiveProps-start');
if (newProps.nextColor) {
this.setState(function(state) {
this.peekAtState('before-setState-receiveProps', state);
return { color: newProps.nextColor };
});
// No longer a public API, but we can test that it works internally by
// reaching into the updater.
// this.updater.enqueueReplaceState(this, {color: undefined});
this.setState(function(state) {
this.peekAtState('before-setState-again-receiveProps', state);
return { color: newProps.nextColor };
}, this.peekAtCallback('setState-receiveProps'));
this.setState(function(state) {
this.peekAtState('after-setState-receiveProps', state);
});
}
this.peekAtState('componentWillReceiveProps-end');
}
shouldComponentUpdate(nextProps, nextState) {
this.peekAtState('shouldComponentUpdate-currentState');
this.peekAtState('shouldComponentUpdate-nextState', nextState);
return true;
}
UNSAFE_componentWillUpdate(nextProps, nextState) {
this.peekAtState('componentWillUpdate-currentState');
this.peekAtState('componentWillUpdate-nextState', nextState);
}
componentDidUpdate(prevProps, prevState) {
this.peekAtState('componentDidUpdate-currentState');
this.peekAtState('componentDidUpdate-prevState', prevState);
}
componentWillUnmount() {
this.peekAtState('componentWillUnmount');
}
};
});
it('should support setting state', () => {
const container = document.createElement('div');
document.body.appendChild(container);
const stateListener = jest.fn();
const instance = ReactDOM.render(
<TestComponent stateListener={stateListener} />,
container,
function peekAtInitialCallback() {
this.peekAtState('initial-callback');
}
);
ReactDOM.render(
<TestComponent stateListener={stateListener} nextColor="green" />,
container,
instance.peekAtCallback('setProps')
);
instance.setFavoriteColor('blue');
instance.forceUpdate(instance.peekAtCallback('forceUpdate'));
ReactDOM.unmountComponentAtNode(container);
let expected = [
// there is no state when getInitialState() is called
['getInitialState', null],
['componentWillMount-start', 'red'],
// setState()'s only enqueue pending states.
['componentWillMount-after-sunrise', 'red'],
['componentWillMount-end', 'red'],
// pending state queue is processed
['before-setState-sunrise', 'red'],
['after-setState-sunrise', 'sunrise'],
['after-setState-orange', 'orange'],
// pending state has been applied
['render', 'orange'],
['componentDidMount-start', 'orange'],
// setState-sunrise and setState-orange should be called here,
// after the bug in #1740
// componentDidMount() called setState({color:'yellow'}), which is async.
// The update doesn't happen until the next flush.
['componentDidMount-end', 'orange'],
['setState-sunrise', 'orange'],
['setState-orange', 'orange'],
['initial-callback', 'orange'],
['shouldComponentUpdate-currentState', 'orange'],
['shouldComponentUpdate-nextState', 'yellow'],
['componentWillUpdate-currentState', 'orange'],
['componentWillUpdate-nextState', 'yellow'],
['render', 'yellow'],
['componentDidUpdate-currentState', 'yellow'],
['componentDidUpdate-prevState', 'orange'],
//** ['componentDidUpdate-prevState', 'yellow'],
['setState-yellow', 'yellow'],
['componentWillReceiveProps-start', 'yellow'],
// setState({color:'green'}) only enqueues a pending state.
['componentWillReceiveProps-end', 'yellow'],
// pending state queue is processed
// We keep updates in the queue to support
// replaceState(prevState => newState).
['before-setState-receiveProps', 'yellow'],
['before-setState-again-receiveProps', 'green'],
['after-setState-receiveProps', 'green'],
['shouldComponentUpdate-currentState', 'yellow'],
['shouldComponentUpdate-nextState', 'green'],
['componentWillUpdate-currentState', 'yellow'],
['componentWillUpdate-nextState', 'green'],
['render', 'green'],
['componentDidUpdate-currentState', 'green'],
['componentDidUpdate-prevState', 'yellow'],
['setState-receiveProps', 'green'],
['setProps', 'green'],
// setFavoriteColor('blue')
['shouldComponentUpdate-currentState', 'green'],
['shouldComponentUpdate-nextState', 'blue'],
['componentWillUpdate-currentState', 'green'],
['componentWillUpdate-nextState', 'blue'],
['render', 'blue'],
['componentDidUpdate-currentState', 'blue'],
//** ['componentDidUpdate-prevState', 'yellow'],
['componentDidUpdate-prevState', 'green'],
['setFavoriteColor', 'blue'],
// forceUpdate()
['componentWillUpdate-currentState', 'blue'],
['componentWillUpdate-nextState', 'blue'],
['render', 'blue'],
['componentDidUpdate-currentState', 'blue'],
['componentDidUpdate-prevState', 'blue'],
//** ['componentDidUpdate-prevState', 'yellow'],
['forceUpdate', 'blue'],
// unmountComponent()
// state is available within `componentWillUnmount()`
['componentWillUnmount', 'blue']
];
expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n'));
});
it('should call componentDidUpdate of children first', () => {
// "暂不测试unstable API"
const container = document.createElement('div');
let ops = [];
let child = null;
let parent = null;
class Child extends React.Component {
state = { bar: false };
componentDidMount() {
child = this;
}
componentDidUpdate() {
ops.push('child did update');
}
render() {
return <div />;
}
}
let shouldUpdate = true;
class Intermediate extends React.Component {
shouldComponentUpdate() {
return shouldUpdate;
}
render() {
return <Child />;
}
}
class Parent extends React.Component {
state = { foo: false };
componentDidMount() {
parent = this;
}
componentDidUpdate() {
ops.push('parent did update');
}
render() {
return <Intermediate />;
}
}
ReactDOM.render(<Parent />, container);
ReactDOM.unstable_batchedUpdates(() => {
parent.setState({ foo: true });
child.setState({ bar: true });
});
// When we render changes top-down in a batch, children's componentDidUpdate
// happens before the parent.
expect(ops).toEqual(['child did update', 'parent did update']);
shouldUpdate = false;
ops = [];
ReactDOM.unstable_batchedUpdates(() => {
parent.setState({ foo: false });
child.setState({ bar: false });
});
// We expect the same thing to happen if we bail out in the middle.
expect(ops).toEqual(['child did update', 'parent did update']);
});
it('should batch unmounts', () => {
let outer;
class Inner extends React.Component {
render() {
return <div />;
}
componentWillUnmount() {
// This should get silently ignored (maybe with a warning), but it
// shouldn't break React.
outer.setState({ showInner: false });
}
}
class Outer extends React.Component {
state = { showInner: true };
render() {
return <div> {this.state.showInner && <Inner />} </div>;
}
}
const container = document.createElement('div');
outer = ReactDOM.render(<Outer />, container);
expect(() => {
ReactDOM.unmountComponentAtNode(container);
}).not.toThrow();
});
it('should update state when called from child cWRP', function() {
const log = [];
class Parent extends React.Component {
state = { value: 'one' };
render() {
log.push('parent render ' + this.state.value);
return <Child parent={this} value={this.state.value} />;
}
}
let updated = false;
class Child extends React.Component {
UNSAFE_componentWillReceiveProps() {
if (updated) {
return;
}
log.push('child componentWillReceiveProps ' + this.props.value);
this.props.parent.setState({ value: 'two' });
log.push('child componentWillReceiveProps done ' + this.props.value);
updated = true;
}
render() {
log.push('child render ' + this.props.value);
return <div> {this.props.value} </div>;
}
}
const container = document.createElement('div');
ReactDOM.render(<Parent />, container);
ReactDOM.render(<Parent />, container);
expect(log).toEqual([
'parent render one',
'child render one',
'parent render one',
'child componentWillReceiveProps one',
'child componentWillReceiveProps done one',
'child render one',
'parent render two',
'child render two'
]);
});
it('should merge state when sCU returns false', function() {
const log = [];
class Test extends React.Component {
state = { a: 0 };
render() {
return null;
}
shouldComponentUpdate(nextProps, nextState) {
log.push('scu from ' + Object.keys(this.state) + ' to ' + Object.keys(nextState));
return false;
}
}
const container = document.createElement('div');
const test = ReactDOM.render(<Test />, container);
test.setState({ b: 0 });
expect(log.length).toBe(1);
test.setState({ c: 0 });
expect(log.length).toBe(2);
expect(log).toEqual(['scu from a to a,b', 'scu from a,b to a,b,c']);
});
it('should treat assigning to this.state inside cWRP as a replaceState, with a warning', () => {
let ops = [];
class Test extends React.Component {
state = { step: 1, extra: true };
UNSAFE_componentWillReceiveProps() {
this.setState({ step: 2 }, () => {
// Tests that earlier setState callbacks are not dropped
ops.push(`callback -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
});
// Treat like replaceState
this.state = { step: 3 };
}
render() {
ops.push(`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
return null;
}
}
// Mount
const container = document.createElement('div');
ReactDOM.render(<Test />, container);
// Update
expect(() => ReactDOM.render(<Test />, container)).toWarnDev(
'Warning: Test.componentWillReceiveProps(): Assigning directly to ' +
'this.state is deprecated (except inside a component\'s constructor). ' +
'Use setState instead.'
);
expect(ops).toEqual([
'render -- step: 1, extra: true',
'render -- step: 2, extra: false',
'callback -- step: 2, extra: false'
]);
// Check deduplication; (no additional warnings are expected)
ReactDOM.render(<Test />, container);
});
it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
let ops = [];
class Test extends React.Component {
state = { step: 1, extra: true };
UNSAFE_componentWillMount() {
this.setState({ step: 2 }, () => {
// Tests that earlier setState callbacks are not dropped
ops.push(`callback -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
});
// Treat like replaceState
this.state = { step: 3 };
}
render() {
ops.push(`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`);
return null;
}
}
// Mount
const container = document.createElement('div');
expect(() => ReactDOM.render(<Test />, container)).toWarnDev(
'Warning: Test.componentWillMount(): Assigning directly to ' +
'this.state is deprecated (except inside a component\'s constructor). ' +
'Use setState instead.'
);
expect(ops).toEqual(['render -- step: 2, extra: false', 'callback -- step: 2, extra: false']);
});
it('should support stateful module pattern components', () => {});
it('should support getDerivedStateFromProps for module pattern components', () => {});
});