Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New class instantiation and initialization process #2808

Merged
merged 1 commit into from
Jan 20, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why stop calling replaceState?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I aim to deprecate it and make a separate queue for all this stuff. Also, it doesn't make sense to have an error message called replaceState when there is no replaceState method on React.Component.

},

/**
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