mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
New class instantiation and initialization process
This allows state to be set up in the constructor instead of through getInitialState. getInitialState is now considered part of "classic". Therefore, they move into ReactClass's constructor. As a consequence of this, we no longer have a mapping between the internal representation and the public instance during the mounting process. Because the constructor hasn't returned yet. We used to have a special case for calling setState in getInitialState which was just ignored. This makes that throw and the component is considered unmounted during the construction phase.
This commit is contained in:
@@ -742,7 +742,8 @@ var ReactClassMixin = {
|
||||
var internalInstance = ReactInstanceMap.get(this);
|
||||
invariant(
|
||||
internalInstance,
|
||||
'setProps(...): Can only update a mounted component.'
|
||||
'setProps(...): Can only update a mounted or mounting component. ' +
|
||||
'This usually means you called setProps() on an unmounted component.'
|
||||
);
|
||||
internalInstance.setProps(
|
||||
partialProps,
|
||||
@@ -797,6 +798,30 @@ var ReactClass = {
|
||||
if (this.__reactAutoBindMap) {
|
||||
bindAutoBindMethods(this);
|
||||
}
|
||||
|
||||
this.props = props;
|
||||
this.state = null;
|
||||
|
||||
// ReactClasses doesn't have constructors. Instead, they use the
|
||||
// getInitialState and componentWillMount methods for initialization.
|
||||
|
||||
var initialState = this.getInitialState ? this.getInitialState() : null;
|
||||
if (__DEV__) {
|
||||
// We allow auto-mocks to proceed as if they're returning null.
|
||||
if (typeof initialState === 'undefined' &&
|
||||
this.getInitialState._isMockFunction) {
|
||||
// This is probably bad practice. Consider warning here and
|
||||
// deprecating this convenience.
|
||||
initialState = null;
|
||||
}
|
||||
}
|
||||
invariant(
|
||||
typeof initialState === 'object' && !Array.isArray(initialState),
|
||||
'%s.getInitialState(): must return an object or null',
|
||||
Constructor.displayName || 'ReactCompositeComponent'
|
||||
);
|
||||
|
||||
this.state = initialState;
|
||||
};
|
||||
Constructor.prototype = new ReactClassBase();
|
||||
Constructor.prototype.constructor = Constructor;
|
||||
@@ -823,9 +848,6 @@ var ReactClass = {
|
||||
if (Constructor.prototype.getInitialState) {
|
||||
Constructor.prototype.getInitialState.isReactClassApproved = {};
|
||||
}
|
||||
if (Constructor.prototype.componentWillMount) {
|
||||
Constructor.prototype.componentWillMount.isReactClassApproved = {};
|
||||
}
|
||||
}
|
||||
|
||||
invariant(
|
||||
|
||||
@@ -40,20 +40,6 @@ function getDeclarationErrorAddendum(component) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function validateLifeCycleOnReplaceState(instance) {
|
||||
var compositeLifeCycleState = instance._compositeLifeCycleState;
|
||||
invariant(
|
||||
ReactCurrentOwner.current == null,
|
||||
'replaceState(...): Cannot update during an existing state transition ' +
|
||||
'(such as within `render`). Render methods should be a pure function ' +
|
||||
'of props and state.'
|
||||
);
|
||||
invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'replaceState(...): Cannot update while unmounting component. This ' +
|
||||
'usually means you called setState() on an unmounted component.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `ReactCompositeComponent` maintains an auxiliary life cycle state in
|
||||
* `this._compositeLifeCycleState` (which can be null).
|
||||
@@ -123,7 +109,8 @@ var ReactCompositeComponentMixin = assign({},
|
||||
this._rootNodeID = null;
|
||||
|
||||
this._instance.props = element.props;
|
||||
this._instance.state = null;
|
||||
// instance.state get set up to its proper initial value in mount
|
||||
// which may be null.
|
||||
this._instance.context = null;
|
||||
this._instance.refs = emptyObject;
|
||||
|
||||
@@ -190,15 +177,7 @@ var ReactCompositeComponentMixin = assign({},
|
||||
}
|
||||
inst.props = this._processProps(this._currentElement.props);
|
||||
|
||||
var initialState = inst.getInitialState ? inst.getInitialState() : null;
|
||||
if (__DEV__) {
|
||||
// We allow auto-mocks to proceed as if they're returning null.
|
||||
if (typeof initialState === 'undefined' &&
|
||||
inst.getInitialState._isMockFunction) {
|
||||
// This is probably bad practice. Consider warning here and
|
||||
// deprecating this convenience.
|
||||
initialState = null;
|
||||
}
|
||||
// Since plain JS classes are defined without any special initialization
|
||||
// logic, we can not catch common errors early. Therefore, we have to
|
||||
// catch them here, at initialization time, instead.
|
||||
@@ -210,14 +189,6 @@ var ReactCompositeComponentMixin = assign({},
|
||||
'Did you mean to define a state property instead?',
|
||||
this.getName() || 'a component'
|
||||
);
|
||||
warning(
|
||||
!inst.componentWillMount ||
|
||||
inst.componentWillMount.isReactClassApproved,
|
||||
'componentWillMount was defined on %s, a plain JavaScript class. ' +
|
||||
'This is only supported for classes created using React.createClass. ' +
|
||||
'Did you mean to define a constructor instead?',
|
||||
this.getName() || 'a component'
|
||||
);
|
||||
warning(
|
||||
!inst.propTypes,
|
||||
'propTypes was defined as an instance property on %s. Use a static ' +
|
||||
@@ -239,9 +210,14 @@ var ReactCompositeComponentMixin = assign({},
|
||||
(this.getName() || 'A component')
|
||||
);
|
||||
}
|
||||
|
||||
var initialState = inst.state;
|
||||
if (initialState === undefined) {
|
||||
inst.state = initialState = null;
|
||||
}
|
||||
invariant(
|
||||
typeof initialState === 'object' && !Array.isArray(initialState),
|
||||
'%s.getInitialState(): must return an object or null',
|
||||
'%s.state: must be set to an object or null',
|
||||
this.getName() || 'ReactCompositeComponent'
|
||||
);
|
||||
inst.state = initialState;
|
||||
@@ -396,11 +372,32 @@ var ReactCompositeComponentMixin = assign({},
|
||||
* @protected
|
||||
*/
|
||||
setState: function(partialState, callback) {
|
||||
// Merge with `_pendingState` if it exists, otherwise with existing state.
|
||||
this.replaceState(
|
||||
assign({}, this._pendingState || this._instance.state, partialState),
|
||||
callback
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
invariant(
|
||||
ReactCurrentOwner.current == null,
|
||||
'setState(...): Cannot update during an existing state transition ' +
|
||||
'(such as within `render`). Render methods should be a pure function ' +
|
||||
'of props and state.'
|
||||
);
|
||||
invariant(
|
||||
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'setState(...): Cannot call setState() on an unmounting component.'
|
||||
);
|
||||
// Merge with `_pendingState` if it exists, otherwise with existing state.
|
||||
this._pendingState = assign(
|
||||
{},
|
||||
this._pendingState || this._instance.state,
|
||||
partialState
|
||||
);
|
||||
if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) {
|
||||
// If we're in a componentWillMount handler, don't enqueue a rerender
|
||||
// because ReactUpdates assumes we're in a browser context (which is wrong
|
||||
// for server rendering) and we're about to do a render anyway.
|
||||
// TODO: The callback here is ignored when setState is called from
|
||||
// componentWillMount. Either fix it or disallow doing so completely in
|
||||
// favor of getInitialState.
|
||||
ReactUpdates.enqueueUpdate(this, callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -416,7 +413,18 @@ var ReactCompositeComponentMixin = assign({},
|
||||
* @protected
|
||||
*/
|
||||
replaceState: function(completeState, callback) {
|
||||
validateLifeCycleOnReplaceState(this);
|
||||
var compositeLifeCycleState = this._compositeLifeCycleState;
|
||||
invariant(
|
||||
ReactCurrentOwner.current == null,
|
||||
'replaceState(...): Cannot update during an existing state transition ' +
|
||||
'(such as within `render`). Render methods should be a pure function ' +
|
||||
'of props and state.'
|
||||
);
|
||||
invariant(
|
||||
compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
|
||||
'replaceState(...): Cannot call replaceState() on an unmounting ' +
|
||||
'component.'
|
||||
);
|
||||
this._pendingState = completeState;
|
||||
if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) {
|
||||
// If we're in a componentWillMount handler, don't enqueue a rerender
|
||||
@@ -1004,22 +1012,15 @@ var ShallowMixin = assign({},
|
||||
// No context for shallow-mounted components.
|
||||
inst.props = this._processProps(this._currentElement.props);
|
||||
|
||||
var initialState = inst.getInitialState ? inst.getInitialState() : null;
|
||||
if (__DEV__) {
|
||||
// We allow auto-mocks to proceed as if they're returning null.
|
||||
if (typeof initialState === 'undefined' &&
|
||||
inst.getInitialState._isMockFunction) {
|
||||
// This is probably bad practice. Consider warning here and
|
||||
// deprecating this convenience.
|
||||
initialState = null;
|
||||
}
|
||||
var initialState = inst.state;
|
||||
if (initialState === undefined) {
|
||||
inst.state = initialState = null;
|
||||
}
|
||||
invariant(
|
||||
typeof initialState === 'object' && !Array.isArray(initialState),
|
||||
'%s.getInitialState(): must return an object or null',
|
||||
'%s.state: must be set to an object or null',
|
||||
this.getName() || 'ReactCompositeComponent'
|
||||
);
|
||||
inst.state = initialState;
|
||||
|
||||
this._pendingState = null;
|
||||
this._pendingForceUpdate = false;
|
||||
|
||||
@@ -104,7 +104,11 @@ describe('ReactComponentLifeCycle', function() {
|
||||
ReactInstanceMap = require('ReactInstanceMap');
|
||||
|
||||
getCompositeLifeCycle = function(instance) {
|
||||
return ReactInstanceMap.get(instance)._compositeLifeCycleState;
|
||||
var internalInstance = ReactInstanceMap.get(instance);
|
||||
if (!internalInstance) {
|
||||
return null;
|
||||
}
|
||||
return internalInstance._compositeLifeCycleState;
|
||||
};
|
||||
|
||||
getLifeCycleState = function(instance) {
|
||||
@@ -221,7 +225,7 @@ describe('ReactComponentLifeCycle', function() {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow update state inside of getInitialState', function() {
|
||||
it('should not allow update state inside of getInitialState', function() {
|
||||
var StatefulComponent = React.createClass({
|
||||
getInitialState: function() {
|
||||
this.setState({stateField: 'something'});
|
||||
@@ -234,16 +238,15 @@ describe('ReactComponentLifeCycle', function() {
|
||||
);
|
||||
}
|
||||
});
|
||||
var instance = <StatefulComponent />;
|
||||
expect(function() {
|
||||
instance = ReactTestUtils.renderIntoDocument(instance);
|
||||
}).not.toThrow();
|
||||
|
||||
// The return value of getInitialState overrides anything from setState
|
||||
expect(instance.state.stateField).toEqual('somethingelse');
|
||||
instance = ReactTestUtils.renderIntoDocument(<StatefulComponent />);
|
||||
}).toThrow(
|
||||
'Invariant Violation: setState(...): Can only update a mounted or ' +
|
||||
'mounting component. This usually means you called setState() on an ' +
|
||||
'unmounted component.'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should carry through each of the phases of setup', function() {
|
||||
var LifeCycleComponent = React.createClass({
|
||||
getInitialState: function() {
|
||||
@@ -317,9 +320,9 @@ describe('ReactComponentLifeCycle', function() {
|
||||
GET_INIT_STATE_RETURN_VAL
|
||||
);
|
||||
expect(instance._testJournal.lifeCycleAtStartOfGetInitialState)
|
||||
.toBe(ComponentLifeCycle.MOUNTED);
|
||||
.toBe(ComponentLifeCycle.UNMOUNTED);
|
||||
expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState)
|
||||
.toBe(CompositeComponentLifeCycle.MOUNTING);
|
||||
.toBe(null);
|
||||
|
||||
// componentWillMount
|
||||
expect(instance._testJournal.stateAtStartOfWillMount).toEqual(
|
||||
|
||||
@@ -343,9 +343,8 @@ describe('ReactCompositeComponent', function() {
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
expect(() => this.setState({ value: 2 })).toThrow(
|
||||
'Invariant Violation: replaceState(...): Cannot update while ' +
|
||||
'unmounting component. This usually means you called setState() ' +
|
||||
'on an unmounted component.'
|
||||
'Invariant Violation: setState(...): Cannot call setState() on an ' +
|
||||
'unmounting component.'
|
||||
);
|
||||
},
|
||||
render: function() {
|
||||
@@ -383,8 +382,9 @@ describe('ReactCompositeComponent', function() {
|
||||
expect(function() {
|
||||
instance.setProps({ value: 2 });
|
||||
}).toThrow(
|
||||
'Invariant Violation: setProps(...): Can only update a mounted ' +
|
||||
'component.'
|
||||
'Invariant Violation: setProps(...): Can only update a mounted or ' +
|
||||
'mounting component. This usually means you called setProps() on an ' +
|
||||
'unmounted component.'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -65,6 +65,129 @@ describe('ReactES6Class', function() {
|
||||
test(<Foo bar="bar" />, 'DIV', 'bar');
|
||||
});
|
||||
|
||||
it('renders based on state using initial values in this.props', function() {
|
||||
class Foo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { bar: this.props.initialValue };
|
||||
}
|
||||
render() {
|
||||
return <span className={this.state.bar} />;
|
||||
}
|
||||
}
|
||||
test(<Foo initialValue="foo" />, 'SPAN', 'foo');
|
||||
});
|
||||
|
||||
it('renders based on state using props in the constructor', function() {
|
||||
class Foo extends React.Component {
|
||||
constructor(props) {
|
||||
this.state = { bar: props.initialValue };
|
||||
}
|
||||
changeState() {
|
||||
this.setState({ bar: 'bar' });
|
||||
}
|
||||
render() {
|
||||
if (this.state.bar === 'foo') {
|
||||
return <div className="foo" />;
|
||||
}
|
||||
return <span className={this.state.bar} />;
|
||||
}
|
||||
}
|
||||
var instance = test(<Foo initialValue="foo" />, 'DIV', 'foo');
|
||||
instance.changeState();
|
||||
test(<Foo />, 'SPAN', 'bar');
|
||||
});
|
||||
|
||||
it('renders only once when setting state in componentWillMount', function() {
|
||||
var renderCount = 0;
|
||||
class Foo extends React.Component {
|
||||
constructor(props) {
|
||||
this.state = { bar: props.initialValue };
|
||||
}
|
||||
componentWillMount() {
|
||||
this.setState({ bar: 'bar' });
|
||||
}
|
||||
render() {
|
||||
renderCount++;
|
||||
return <span className={this.state.bar} />;
|
||||
}
|
||||
}
|
||||
test(<Foo initialValue="foo" />, 'SPAN', 'bar');
|
||||
expect(renderCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw with non-object in the initial state property', function() {
|
||||
[['an array'], 'a string', 1234].forEach(function(state) {
|
||||
class Foo {
|
||||
constructor() {
|
||||
this.state = state;
|
||||
}
|
||||
render() {
|
||||
return <span />;
|
||||
}
|
||||
}
|
||||
expect(() => test(<Foo />, 'span', '')).toThrow(
|
||||
'Invariant Violation: Foo.state: ' +
|
||||
'must be set to an object or null'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render with null in the initial state property', function() {
|
||||
class Foo extends React.Component {
|
||||
constructor() {
|
||||
this.state = null;
|
||||
}
|
||||
render() {
|
||||
return <span />;
|
||||
}
|
||||
}
|
||||
test(<Foo />, 'SPAN', '');
|
||||
});
|
||||
|
||||
it('setState through an event handler', function() {
|
||||
class Foo extends React.Component {
|
||||
constructor(props) {
|
||||
this.state = { bar: props.initialValue };
|
||||
}
|
||||
handleClick() {
|
||||
this.setState({ bar: 'bar' });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Inner
|
||||
name={this.state.bar}
|
||||
onClick={this.handleClick.bind(this)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
test(<Foo initialValue="foo" />, 'DIV', 'foo');
|
||||
attachedListener();
|
||||
expect(renderedName).toBe('bar');
|
||||
});
|
||||
|
||||
it('should not implicitly bind event handlers', function() {
|
||||
class Foo extends React.Component {
|
||||
constructor(props) {
|
||||
this.state = { bar: props.initialValue };
|
||||
}
|
||||
handleClick() {
|
||||
this.setState({ bar: 'bar' });
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Inner
|
||||
name={this.state.bar}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
test(<Foo initialValue="foo" />, 'DIV', 'foo');
|
||||
expect(attachedListener).toThrow();
|
||||
});
|
||||
|
||||
it('renders using forceUpdate even when there is no state', function() {
|
||||
class Foo extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -88,11 +211,62 @@ describe('ReactES6Class', function() {
|
||||
expect(renderedName).toBe('bar');
|
||||
});
|
||||
|
||||
it('will call all the normal life cycle methods', function() {
|
||||
var lifeCycles = [];
|
||||
class Foo {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
}
|
||||
componentWillMount() {
|
||||
lifeCycles.push('will-mount');
|
||||
}
|
||||
componentDidMount() {
|
||||
lifeCycles.push('did-mount');
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
lifeCycles.push('receive-props', nextProps);
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
lifeCycles.push('should-update', nextProps, nextState);
|
||||
return true;
|
||||
}
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
lifeCycles.push('will-update', nextProps, nextState);
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
lifeCycles.push('did-update', prevProps, prevState);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
lifeCycles.push('will-unmount');
|
||||
}
|
||||
render() {
|
||||
return <span className={this.props.value} />;
|
||||
}
|
||||
}
|
||||
var instance = test(<Foo value="foo" />, 'SPAN', 'foo');
|
||||
expect(lifeCycles).toEqual([
|
||||
'will-mount',
|
||||
'did-mount'
|
||||
]);
|
||||
lifeCycles = []; // reset
|
||||
test(<Foo value="bar" />, 'SPAN', 'bar');
|
||||
expect(lifeCycles).toEqual([
|
||||
'receive-props', { value: 'bar' },
|
||||
'should-update', { value: 'bar' }, {},
|
||||
'will-update', { value: 'bar' }, {},
|
||||
'did-update', { value: 'foo' }, {}
|
||||
]);
|
||||
lifeCycles = []; // reset
|
||||
React.unmountComponentAtNode(container);
|
||||
expect(lifeCycles).toEqual([
|
||||
'will-unmount'
|
||||
]);
|
||||
});
|
||||
|
||||
it('warns when classic properties are defined on the instance, ' +
|
||||
'but does not invoke them.', function() {
|
||||
spyOn(console, 'warn');
|
||||
var getInitialStateWasCalled = false;
|
||||
var componentWillMountWasCalled = false;
|
||||
class Foo extends React.Component {
|
||||
constructor() {
|
||||
this.contextTypes = {};
|
||||
@@ -102,27 +276,20 @@ describe('ReactES6Class', function() {
|
||||
getInitialStateWasCalled = true;
|
||||
return {};
|
||||
}
|
||||
componentWillMount() {
|
||||
componentWillMountWasCalled = true;
|
||||
}
|
||||
render() {
|
||||
return <span className="foo" />;
|
||||
}
|
||||
}
|
||||
test(<Foo />, 'SPAN', 'foo');
|
||||
// TODO: expect(getInitialStateWasCalled).toBe(false);
|
||||
// TODO: expect(componentWillMountWasCalled).toBe(false);
|
||||
expect(console.warn.calls.length).toBe(4);
|
||||
expect(getInitialStateWasCalled).toBe(false);
|
||||
expect(console.warn.calls.length).toBe(3);
|
||||
expect(console.warn.calls[0].args[0]).toContain(
|
||||
'getInitialState was defined on Foo, a plain JavaScript class.'
|
||||
);
|
||||
expect(console.warn.calls[1].args[0]).toContain(
|
||||
'componentWillMount was defined on Foo, a plain JavaScript class.'
|
||||
);
|
||||
expect(console.warn.calls[2].args[0]).toContain(
|
||||
'propTypes was defined as an instance property on Foo.'
|
||||
);
|
||||
expect(console.warn.calls[3].args[0]).toContain(
|
||||
expect(console.warn.calls[2].args[0]).toContain(
|
||||
'contextTypes was defined as an instance property on Foo.'
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user