466 lines
14 KiB
JavaScript
466 lines
14 KiB
JavaScript
/**
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @emails react-core
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var React;
|
|
var ReactDOM;
|
|
|
|
var 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>;
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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', () => {
|
|
var container = document.createElement('div');
|
|
document.body.appendChild(container);
|
|
|
|
var stateListener = jest.fn();
|
|
var 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'],
|
|
['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', undefined],
|
|
['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', 'green'],
|
|
['setFavoriteColor', 'blue'],
|
|
// forceUpdate()
|
|
['componentWillUpdate-currentState', 'blue'],
|
|
['componentWillUpdate-nextState', 'blue'],
|
|
['render', 'blue'],
|
|
['componentDidUpdate-currentState', 'blue'],
|
|
['componentDidUpdate-prevState', 'blue'],
|
|
['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', () => {
|
|
var container = document.createElement('div');
|
|
|
|
var ops = [];
|
|
|
|
var child = null;
|
|
var parent = null;
|
|
|
|
class Child extends React.Component {
|
|
state = {bar: false};
|
|
componentDidMount() {
|
|
child = this;
|
|
}
|
|
componentDidUpdate() {
|
|
ops.push('child did update');
|
|
}
|
|
render() {
|
|
return <div />;
|
|
}
|
|
}
|
|
|
|
var 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', () => {
|
|
var 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>;
|
|
}
|
|
}
|
|
|
|
var 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 {
|
|
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>;
|
|
}
|
|
}
|
|
var 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', () => {
|
|
spyOn(console, 'error');
|
|
|
|
let ops = [];
|
|
class Test extends React.Component {
|
|
state = {step: 1, extra: true};
|
|
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
|
|
ReactDOM.render(<Test />, container);
|
|
|
|
expect(ops).toEqual([
|
|
'render -- step: 1, extra: true',
|
|
'render -- step: 3, extra: false',
|
|
'callback -- step: 3, extra: false',
|
|
]);
|
|
expect(console.error.calls.count()).toEqual(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toEqual(
|
|
'Warning: Test.componentWillReceiveProps(): Assigning directly to ' +
|
|
"this.state is deprecated (except inside a component's constructor). " +
|
|
'Use setState instead.',
|
|
);
|
|
});
|
|
|
|
it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
|
|
spyOn(console, 'error');
|
|
|
|
let ops = [];
|
|
class Test extends React.Component {
|
|
state = {step: 1, extra: true};
|
|
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');
|
|
ReactDOM.render(<Test />, container);
|
|
|
|
expect(ops).toEqual([
|
|
'render -- step: 3, extra: false',
|
|
'callback -- step: 3, extra: false',
|
|
]);
|
|
expect(console.error.calls.count()).toEqual(1);
|
|
expect(console.error.calls.argsFor(0)[0]).toEqual(
|
|
'Warning: Test.componentWillMount(): Assigning directly to ' +
|
|
"this.state is deprecated (except inside a component's constructor). " +
|
|
'Use setState instead.',
|
|
);
|
|
});
|
|
});
|