Skip to content

Commit

Permalink
Merge pull request #2808 from sebmarkbage/modernclasses
Browse files Browse the repository at this point in the history
New class instantiation and initialization process
  • Loading branch information
sebmarkbage committed Jan 20, 2015
2 parents f92967c + 291a92c commit 2d75b11
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 78 deletions.
30 changes: 26 additions & 4 deletions src/classic/class/ReactClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -823,9 +848,6 @@ var ReactClass = {
if (Constructor.prototype.getInitialState) {
Constructor.prototype.getInitialState.isReactClassApproved = {};
}
if (Constructor.prototype.componentWillMount) {
Constructor.prototype.componentWillMount.isReactClassApproved = {};
}
}

invariant(
Expand Down
95 changes: 48 additions & 47 deletions src/core/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -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 ' +
Expand All @@ -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;
Expand Down Expand Up @@ -396,11 +372,32 @@ var ReactCompositeComponentMixin = assign({},
* @protected
*/
setState: function(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.replaceState(
assign({}, this._pendingState || this._instance.state, partialState),
callback
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);
}
},

/**
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down
25 changes: 14 additions & 11 deletions src/core/__tests__/ReactComponentLifeCycle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'});
Expand All @@ -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() {
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 5 additions & 5 deletions src/core/__tests__/ReactCompositeComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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.'
);
});

Expand Down
Loading

0 comments on commit 2d75b11

Please sign in to comment.