Merge pull request #2808 from sebmarkbage/modernclasses

New class instantiation and initialization process
This commit is contained in:
Sebastian Markbåge 2015-01-20 14:28:40 -08:00
commit 2d75b11097
5 changed files with 272 additions and 79 deletions

View File

@ -742,7 +742,8 @@ var ReactClassMixin = {
var internalInstance = ReactInstanceMap.get(this); var internalInstance = ReactInstanceMap.get(this);
invariant( invariant(
internalInstance, 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( internalInstance.setProps(
partialProps, partialProps,
@ -797,6 +798,30 @@ var ReactClass = {
if (this.__reactAutoBindMap) { if (this.__reactAutoBindMap) {
bindAutoBindMethods(this); 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 = new ReactClassBase();
Constructor.prototype.constructor = Constructor; Constructor.prototype.constructor = Constructor;
@ -823,9 +848,6 @@ var ReactClass = {
if (Constructor.prototype.getInitialState) { if (Constructor.prototype.getInitialState) {
Constructor.prototype.getInitialState.isReactClassApproved = {}; Constructor.prototype.getInitialState.isReactClassApproved = {};
} }
if (Constructor.prototype.componentWillMount) {
Constructor.prototype.componentWillMount.isReactClassApproved = {};
}
} }
invariant( invariant(

View File

@ -40,20 +40,6 @@ function getDeclarationErrorAddendum(component) {
return ''; 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 * `ReactCompositeComponent` maintains an auxiliary life cycle state in
* `this._compositeLifeCycleState` (which can be null). * `this._compositeLifeCycleState` (which can be null).
@ -123,7 +109,8 @@ var ReactCompositeComponentMixin = assign({},
this._rootNodeID = null; this._rootNodeID = null;
this._instance.props = element.props; 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.context = null;
this._instance.refs = emptyObject; this._instance.refs = emptyObject;
@ -190,15 +177,7 @@ var ReactCompositeComponentMixin = assign({},
} }
inst.props = this._processProps(this._currentElement.props); inst.props = this._processProps(this._currentElement.props);
var initialState = inst.getInitialState ? inst.getInitialState() : null;
if (__DEV__) { 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 // Since plain JS classes are defined without any special initialization
// logic, we can not catch common errors early. Therefore, we have to // logic, we can not catch common errors early. Therefore, we have to
// catch them here, at initialization time, instead. // catch them here, at initialization time, instead.
@ -210,14 +189,6 @@ var ReactCompositeComponentMixin = assign({},
'Did you mean to define a state property instead?', 'Did you mean to define a state property instead?',
this.getName() || 'a component' 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( warning(
!inst.propTypes, !inst.propTypes,
'propTypes was defined as an instance property on %s. Use a static ' + 'propTypes was defined as an instance property on %s. Use a static ' +
@ -239,9 +210,14 @@ var ReactCompositeComponentMixin = assign({},
(this.getName() || 'A component') (this.getName() || 'A component')
); );
} }
var initialState = inst.state;
if (initialState === undefined) {
inst.state = initialState = null;
}
invariant( invariant(
typeof initialState === 'object' && !Array.isArray(initialState), 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' this.getName() || 'ReactCompositeComponent'
); );
inst.state = initialState; inst.state = initialState;
@ -396,11 +372,32 @@ var ReactCompositeComponentMixin = assign({},
* @protected * @protected
*/ */
setState: function(partialState, callback) { setState: function(partialState, callback) {
// Merge with `_pendingState` if it exists, otherwise with existing state. var compositeLifeCycleState = this._compositeLifeCycleState;
this.replaceState( invariant(
assign({}, this._pendingState || this._instance.state, partialState), ReactCurrentOwner.current == null,
callback '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 * @protected
*/ */
replaceState: function(completeState, callback) { 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; this._pendingState = completeState;
if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) { if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) {
// If we're in a componentWillMount handler, don't enqueue a rerender // 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. // No context for shallow-mounted components.
inst.props = this._processProps(this._currentElement.props); inst.props = this._processProps(this._currentElement.props);
var initialState = inst.getInitialState ? inst.getInitialState() : null; var initialState = inst.state;
if (__DEV__) { if (initialState === undefined) {
// We allow auto-mocks to proceed as if they're returning null. inst.state = initialState = null;
if (typeof initialState === 'undefined' &&
inst.getInitialState._isMockFunction) {
// This is probably bad practice. Consider warning here and
// deprecating this convenience.
initialState = null;
}
} }
invariant( invariant(
typeof initialState === 'object' && !Array.isArray(initialState), 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' this.getName() || 'ReactCompositeComponent'
); );
inst.state = initialState;
this._pendingState = null; this._pendingState = null;
this._pendingForceUpdate = false; this._pendingForceUpdate = false;

View File

@ -104,7 +104,11 @@ describe('ReactComponentLifeCycle', function() {
ReactInstanceMap = require('ReactInstanceMap'); ReactInstanceMap = require('ReactInstanceMap');
getCompositeLifeCycle = function(instance) { getCompositeLifeCycle = function(instance) {
return ReactInstanceMap.get(instance)._compositeLifeCycleState; var internalInstance = ReactInstanceMap.get(instance);
if (!internalInstance) {
return null;
}
return internalInstance._compositeLifeCycleState;
}; };
getLifeCycleState = function(instance) { getLifeCycleState = function(instance) {
@ -221,7 +225,7 @@ describe('ReactComponentLifeCycle', function() {
}).not.toThrow(); }).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({ var StatefulComponent = React.createClass({
getInitialState: function() { getInitialState: function() {
this.setState({stateField: 'something'}); this.setState({stateField: 'something'});
@ -234,16 +238,15 @@ describe('ReactComponentLifeCycle', function() {
); );
} }
}); });
var instance = <StatefulComponent />;
expect(function() { expect(function() {
instance = ReactTestUtils.renderIntoDocument(instance); instance = ReactTestUtils.renderIntoDocument(<StatefulComponent />);
}).not.toThrow(); }).toThrow(
'Invariant Violation: setState(...): Can only update a mounted or ' +
// The return value of getInitialState overrides anything from setState 'mounting component. This usually means you called setState() on an ' +
expect(instance.state.stateField).toEqual('somethingelse'); 'unmounted component.'
);
}); });
it('should carry through each of the phases of setup', function() { it('should carry through each of the phases of setup', function() {
var LifeCycleComponent = React.createClass({ var LifeCycleComponent = React.createClass({
getInitialState: function() { getInitialState: function() {
@ -317,9 +320,9 @@ describe('ReactComponentLifeCycle', function() {
GET_INIT_STATE_RETURN_VAL GET_INIT_STATE_RETURN_VAL
); );
expect(instance._testJournal.lifeCycleAtStartOfGetInitialState) expect(instance._testJournal.lifeCycleAtStartOfGetInitialState)
.toBe(ComponentLifeCycle.MOUNTED); .toBe(ComponentLifeCycle.UNMOUNTED);
expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState) expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState)
.toBe(CompositeComponentLifeCycle.MOUNTING); .toBe(null);
// componentWillMount // componentWillMount
expect(instance._testJournal.stateAtStartOfWillMount).toEqual( expect(instance._testJournal.stateAtStartOfWillMount).toEqual(

View File

@ -343,9 +343,8 @@ describe('ReactCompositeComponent', function() {
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
expect(() => this.setState({ value: 2 })).toThrow( expect(() => this.setState({ value: 2 })).toThrow(
'Invariant Violation: replaceState(...): Cannot update while ' + 'Invariant Violation: setState(...): Cannot call setState() on an ' +
'unmounting component. This usually means you called setState() ' + 'unmounting component.'
'on an unmounted component.'
); );
}, },
render: function() { render: function() {
@ -383,8 +382,9 @@ describe('ReactCompositeComponent', function() {
expect(function() { expect(function() {
instance.setProps({ value: 2 }); instance.setProps({ value: 2 });
}).toThrow( }).toThrow(
'Invariant Violation: setProps(...): Can only update a mounted ' + 'Invariant Violation: setProps(...): Can only update a mounted or ' +
'component.' 'mounting component. This usually means you called setProps() on an ' +
'unmounted component.'
); );
}); });

View File

@ -65,6 +65,129 @@ describe('ReactES6Class', function() {
test(<Foo bar="bar" />, 'DIV', 'bar'); 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() { it('renders using forceUpdate even when there is no state', function() {
class Foo extends React.Component { class Foo extends React.Component {
constructor(props) { constructor(props) {
@ -88,11 +211,62 @@ describe('ReactES6Class', function() {
expect(renderedName).toBe('bar'); 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, ' + it('warns when classic properties are defined on the instance, ' +
'but does not invoke them.', function() { 'but does not invoke them.', function() {
spyOn(console, 'warn'); spyOn(console, 'warn');
var getInitialStateWasCalled = false; var getInitialStateWasCalled = false;
var componentWillMountWasCalled = false;
class Foo extends React.Component { class Foo extends React.Component {
constructor() { constructor() {
this.contextTypes = {}; this.contextTypes = {};
@ -102,27 +276,20 @@ describe('ReactES6Class', function() {
getInitialStateWasCalled = true; getInitialStateWasCalled = true;
return {}; return {};
} }
componentWillMount() {
componentWillMountWasCalled = true;
}
render() { render() {
return <span className="foo" />; return <span className="foo" />;
} }
} }
test(<Foo />, 'SPAN', 'foo'); test(<Foo />, 'SPAN', 'foo');
// TODO: expect(getInitialStateWasCalled).toBe(false); expect(getInitialStateWasCalled).toBe(false);
// TODO: expect(componentWillMountWasCalled).toBe(false); expect(console.warn.calls.length).toBe(3);
expect(console.warn.calls.length).toBe(4);
expect(console.warn.calls[0].args[0]).toContain( expect(console.warn.calls[0].args[0]).toContain(
'getInitialState was defined on Foo, a plain JavaScript class.' 'getInitialState was defined on Foo, a plain JavaScript class.'
); );
expect(console.warn.calls[1].args[0]).toContain( 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.' '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.' 'contextTypes was defined as an instance property on Foo.'
); );
}); });