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.
Sebastian Markbage committed
Jan 20, 2015 at 14:27 UTC
291a92c4a6d8a3b00121f2fa8cf398cf22d73449
5 files changed
+271
-78
src/classic/class/ReactClass.js
+26
-4
@@ -742,7 +742,8 @@ var ReactClassMixin = {
742
var internalInstance = ReactInstanceMap.get(this);
743
invariant(
744
internalInstance,
745
- 'setProps(...): Can only update a mounted component.'
745
+ 'setProps(...): Can only update a mounted or mounting component. ' +
746
+ 'This usually means you called setProps() on an unmounted component.'
747
);
748
internalInstance.setProps(
749
partialProps,
@@ -797,6 +798,30 @@ var ReactClass = {
798
if (this.__reactAutoBindMap) {
799
bindAutoBindMethods(this);
800
}
801
+
802
+ this.props = props;
803
+ this.state = null;
804
+
805
+ // ReactClasses doesn't have constructors. Instead, they use the
806
+ // getInitialState and componentWillMount methods for initialization.
807
+
808
+ var initialState = this.getInitialState ? this.getInitialState() : null;
809
+ if (__DEV__) {
810
+ // We allow auto-mocks to proceed as if they're returning null.
811
+ if (typeof initialState === 'undefined' &&
812
+ this.getInitialState._isMockFunction) {
813
+ // This is probably bad practice. Consider warning here and
814
+ // deprecating this convenience.
815
+ initialState = null;
816
+ }
817
+ }
818
+ invariant(
819
+ typeof initialState === 'object' && !Array.isArray(initialState),
820
+ '%s.getInitialState(): must return an object or null',
821
+ Constructor.displayName || 'ReactCompositeComponent'
822
+ );
823
+
824
+ this.state = initialState;
825
};
826
Constructor.prototype = new ReactClassBase();
827
Constructor.prototype.constructor = Constructor;
@@ -823,9 +848,6 @@ var ReactClass = {
848
if (Constructor.prototype.getInitialState) {
849
Constructor.prototype.getInitialState.isReactClassApproved = {};
850
}
826
- if (Constructor.prototype.componentWillMount) {
827
- Constructor.prototype.componentWillMount.isReactClassApproved = {};
828
- }
851
}
852
853
invariant(
src/core/ReactCompositeComponent.js
+48
-47
@@ -40,20 +40,6 @@ function getDeclarationErrorAddendum(component) {
40
return '';
41
}
42
43
-function validateLifeCycleOnReplaceState(instance) {
44
- var compositeLifeCycleState = instance._compositeLifeCycleState;
45
- invariant(
46
- ReactCurrentOwner.current == null,
47
- 'replaceState(...): Cannot update during an existing state transition ' +
48
- '(such as within `render`). Render methods should be a pure function ' +
49
- 'of props and state.'
50
- );
51
- invariant(compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
52
- 'replaceState(...): Cannot update while unmounting component. This ' +
53
- 'usually means you called setState() on an unmounted component.'
54
- );
55
-}
56
-
43
/**
44
* `ReactCompositeComponent` maintains an auxiliary life cycle state in
45
* `this._compositeLifeCycleState` (which can be null).
@@ -123,7 +109,8 @@ var ReactCompositeComponentMixin = assign({},
109
this._rootNodeID = null;
110
111
this._instance.props = element.props;
126
- this._instance.state = null;
112
+ // instance.state get set up to its proper initial value in mount
113
+ // which may be null.
114
this._instance.context = null;
115
this._instance.refs = emptyObject;
116
@@ -190,15 +177,7 @@ var ReactCompositeComponentMixin = assign({},
177
}
178
inst.props = this._processProps(this._currentElement.props);
179
193
- var initialState = inst.getInitialState ? inst.getInitialState() : null;
180
if (__DEV__) {
195
- // We allow auto-mocks to proceed as if they're returning null.
196
- if (typeof initialState === 'undefined' &&
197
- inst.getInitialState._isMockFunction) {
198
- // This is probably bad practice. Consider warning here and
199
- // deprecating this convenience.
200
- initialState = null;
201
- }
181
// Since plain JS classes are defined without any special initialization
182
// logic, we can not catch common errors early. Therefore, we have to
183
// catch them here, at initialization time, instead.
@@ -210,14 +189,6 @@ var ReactCompositeComponentMixin = assign({},
189
'Did you mean to define a state property instead?',
190
this.getName() || 'a component'
191
);
213
- warning(
214
- !inst.componentWillMount ||
215
- inst.componentWillMount.isReactClassApproved,
216
- 'componentWillMount was defined on %s, a plain JavaScript class. ' +
217
- 'This is only supported for classes created using React.createClass. ' +
218
- 'Did you mean to define a constructor instead?',
219
- this.getName() || 'a component'
220
- );
192
warning(
193
!inst.propTypes,
194
'propTypes was defined as an instance property on %s. Use a static ' +
@@ -239,9 +210,14 @@ var ReactCompositeComponentMixin = assign({},
210
(this.getName() || 'A component')
211
);
212
}
213
+
214
+ var initialState = inst.state;
215
+ if (initialState === undefined) {
216
+ inst.state = initialState = null;
217
+ }
218
invariant(
219
typeof initialState === 'object' && !Array.isArray(initialState),
244
- '%s.getInitialState(): must return an object or null',
220
+ '%s.state: must be set to an object or null',
221
this.getName() || 'ReactCompositeComponent'
222
);
223
inst.state = initialState;
@@ -396,11 +372,32 @@ var ReactCompositeComponentMixin = assign({},
372
* @protected
373
*/
374
setState: function(partialState, callback) {
375
+ var compositeLifeCycleState = this._compositeLifeCycleState;
376
+ invariant(
377
+ ReactCurrentOwner.current == null,
378
+ 'setState(...): Cannot update during an existing state transition ' +
379
+ '(such as within `render`). Render methods should be a pure function ' +
380
+ 'of props and state.'
381
+ );
382
+ invariant(
383
+ compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
384
+ 'setState(...): Cannot call setState() on an unmounting component.'
385
+ );
386
// Merge with `_pendingState` if it exists, otherwise with existing state.
400
- this.replaceState(
401
- assign({}, this._pendingState || this._instance.state, partialState),
402
- callback
387
+ this._pendingState = assign(
388
+ {},
389
+ this._pendingState || this._instance.state,
390
+ partialState
391
);
392
+ if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) {
393
+ // If we're in a componentWillMount handler, don't enqueue a rerender
394
+ // because ReactUpdates assumes we're in a browser context (which is wrong
395
+ // for server rendering) and we're about to do a render anyway.
396
+ // TODO: The callback here is ignored when setState is called from
397
+ // componentWillMount. Either fix it or disallow doing so completely in
398
+ // favor of getInitialState.
399
+ ReactUpdates.enqueueUpdate(this, callback);
400
+ }
401
},
402
403
/**
@@ -416,7 +413,18 @@ var ReactCompositeComponentMixin = assign({},
413
* @protected
414
*/
415
replaceState: function(completeState, callback) {
419
- validateLifeCycleOnReplaceState(this);
416
+ var compositeLifeCycleState = this._compositeLifeCycleState;
417
+ invariant(
418
+ ReactCurrentOwner.current == null,
419
+ 'replaceState(...): Cannot update during an existing state transition ' +
420
+ '(such as within `render`). Render methods should be a pure function ' +
421
+ 'of props and state.'
422
+ );
423
+ invariant(
424
+ compositeLifeCycleState !== CompositeLifeCycle.UNMOUNTING,
425
+ 'replaceState(...): Cannot call replaceState() on an unmounting ' +
426
+ 'component.'
427
+ );
428
this._pendingState = completeState;
429
if (this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING) {
430
// If we're in a componentWillMount handler, don't enqueue a rerender
@@ -1004,22 +1012,15 @@ var ShallowMixin = assign({},
1012
// No context for shallow-mounted components.
1013
inst.props = this._processProps(this._currentElement.props);
1014
1007
- var initialState = inst.getInitialState ? inst.getInitialState() : null;
1008
- if (__DEV__) {
1009
- // We allow auto-mocks to proceed as if they're returning null.
1010
- if (typeof initialState === 'undefined' &&
1011
- inst.getInitialState._isMockFunction) {
1012
- // This is probably bad practice. Consider warning here and
1013
- // deprecating this convenience.
1014
- initialState = null;
1015
- }
1015
+ var initialState = inst.state;
1016
+ if (initialState === undefined) {
1017
+ inst.state = initialState = null;
1018
}
1019
invariant(
1020
typeof initialState === 'object' && !Array.isArray(initialState),
1019
- '%s.getInitialState(): must return an object or null',
1021
+ '%s.state: must be set to an object or null',
1022
this.getName() || 'ReactCompositeComponent'
1023
);
1022
- inst.state = initialState;
1024
1025
this._pendingState = null;
1026
this._pendingForceUpdate = false;
src/core/__tests__/ReactComponentLifeCycle-test.js
+14
-11
@@ -104,7 +104,11 @@ describe('ReactComponentLifeCycle', function() {
104
ReactInstanceMap = require('ReactInstanceMap');
105
106
getCompositeLifeCycle = function(instance) {
107
- return ReactInstanceMap.get(instance)._compositeLifeCycleState;
107
+ var internalInstance = ReactInstanceMap.get(instance);
108
+ if (!internalInstance) {
109
+ return null;
110
+ }
111
+ return internalInstance._compositeLifeCycleState;
112
};
113
114
getLifeCycleState = function(instance) {
@@ -221,7 +225,7 @@ describe('ReactComponentLifeCycle', function() {
225
}).not.toThrow();
226
});
227
224
- it('should allow update state inside of getInitialState', function() {
228
+ it('should not allow update state inside of getInitialState', function() {
229
var StatefulComponent = React.createClass({
230
getInitialState: function() {
231
this.setState({stateField: 'something'});
@@ -234,16 +238,15 @@ describe('ReactComponentLifeCycle', function() {
238
);
239
}
240
});
237
- var instance = <StatefulComponent />;
241
expect(function() {
239
- instance = ReactTestUtils.renderIntoDocument(instance);
240
- }).not.toThrow();
241
-
242
- // The return value of getInitialState overrides anything from setState
243
- expect(instance.state.stateField).toEqual('somethingelse');
242
+ instance = ReactTestUtils.renderIntoDocument(<StatefulComponent />);
243
+ }).toThrow(
244
+ 'Invariant Violation: setState(...): Can only update a mounted or ' +
245
+ 'mounting component. This usually means you called setState() on an ' +
246
+ 'unmounted component.'
247
+ );
248
});
249
246
-
250
it('should carry through each of the phases of setup', function() {
251
var LifeCycleComponent = React.createClass({
252
getInitialState: function() {
@@ -317,9 +320,9 @@ describe('ReactComponentLifeCycle', function() {
320
GET_INIT_STATE_RETURN_VAL
321
);
322
expect(instance._testJournal.lifeCycleAtStartOfGetInitialState)
320
- .toBe(ComponentLifeCycle.MOUNTED);
323
+ .toBe(ComponentLifeCycle.UNMOUNTED);
324
expect(instance._testJournal.compositeLifeCycleAtStartOfGetInitialState)
322
- .toBe(CompositeComponentLifeCycle.MOUNTING);
325
+ .toBe(null);
326
327
// componentWillMount
328
expect(instance._testJournal.stateAtStartOfWillMount).toEqual(
src/core/__tests__/ReactCompositeComponent-test.js
+5
-5
@@ -343,9 +343,8 @@ describe('ReactCompositeComponent', function() {
343
},
344
componentWillUnmount: function() {
345
expect(() => this.setState({ value: 2 })).toThrow(
346
- 'Invariant Violation: replaceState(...): Cannot update while ' +
347
- 'unmounting component. This usually means you called setState() ' +
348
- 'on an unmounted component.'
346
+ 'Invariant Violation: setState(...): Cannot call setState() on an ' +
347
+ 'unmounting component.'
348
);
349
},
350
render: function() {
@@ -383,8 +382,9 @@ describe('ReactCompositeComponent', function() {
382
expect(function() {
383
instance.setProps({ value: 2 });
384
}).toThrow(
386
- 'Invariant Violation: setProps(...): Can only update a mounted ' +
387
- 'component.'
385
+ 'Invariant Violation: setProps(...): Can only update a mounted or ' +
386
+ 'mounting component. This usually means you called setProps() on an ' +
387
+ 'unmounted component.'
388
);
389
});
390
src/modern/class/__tests__/ReactES6Class-test.js
+178
-11
@@ -65,6 +65,129 @@ describe('ReactES6Class', function() {
65
test(<Foo bar="bar" />, 'DIV', 'bar');
66
});
67
68
+ it('renders based on state using initial values in this.props', function() {
69
+ class Foo extends React.Component {
70
+ constructor(props) {
71
+ super(props);
72
+ this.state = { bar: this.props.initialValue };
73
+ }
74
+ render() {
75
+ return <span className={this.state.bar} />;
76
+ }
77
+ }
78
+ test(<Foo initialValue="foo" />, 'SPAN', 'foo');
79
+ });
80
+
81
+ it('renders based on state using props in the constructor', function() {
82
+ class Foo extends React.Component {
83
+ constructor(props) {
84
+ this.state = { bar: props.initialValue };
85
+ }
86
+ changeState() {
87
+ this.setState({ bar: 'bar' });
88
+ }
89
+ render() {
90
+ if (this.state.bar === 'foo') {
91
+ return <div className="foo" />;
92
+ }
93
+ return <span className={this.state.bar} />;
94
+ }
95
+ }
96
+ var instance = test(<Foo initialValue="foo" />, 'DIV', 'foo');
97
+ instance.changeState();
98
+ test(<Foo />, 'SPAN', 'bar');
99
+ });
100
+
101
+ it('renders only once when setting state in componentWillMount', function() {
102
+ var renderCount = 0;
103
+ class Foo extends React.Component {
104
+ constructor(props) {
105
+ this.state = { bar: props.initialValue };
106
+ }
107
+ componentWillMount() {
108
+ this.setState({ bar: 'bar' });
109
+ }
110
+ render() {
111
+ renderCount++;
112
+ return <span className={this.state.bar} />;
113
+ }
114
+ }
115
+ test(<Foo initialValue="foo" />, 'SPAN', 'bar');
116
+ expect(renderCount).toBe(1);
117
+ });
118
+
119
+ it('should throw with non-object in the initial state property', function() {
120
+ [['an array'], 'a string', 1234].forEach(function(state) {
121
+ class Foo {
122
+ constructor() {
123
+ this.state = state;
124
+ }
125
+ render() {
126
+ return <span />;
127
+ }
128
+ }
129
+ expect(() => test(<Foo />, 'span', '')).toThrow(
130
+ 'Invariant Violation: Foo.state: ' +
131
+ 'must be set to an object or null'
132
+ );
133
+ });
134
+ });
135
+
136
+ it('should render with null in the initial state property', function() {
137
+ class Foo extends React.Component {
138
+ constructor() {
139
+ this.state = null;
140
+ }
141
+ render() {
142
+ return <span />;
143
+ }
144
+ }
145
+ test(<Foo />, 'SPAN', '');
146
+ });
147
+
148
+ it('setState through an event handler', function() {
149
+ class Foo extends React.Component {
150
+ constructor(props) {
151
+ this.state = { bar: props.initialValue };
152
+ }
153
+ handleClick() {
154
+ this.setState({ bar: 'bar' });
155
+ }
156
+ render() {
157
+ return (
158
+ <Inner
159
+ name={this.state.bar}
160
+ onClick={this.handleClick.bind(this)}
161
+ />
162
+ );
163
+ }
164
+ }
165
+ test(<Foo initialValue="foo" />, 'DIV', 'foo');
166
+ attachedListener();
167
+ expect(renderedName).toBe('bar');
168
+ });
169
+
170
+ it('should not implicitly bind event handlers', function() {
171
+ class Foo extends React.Component {
172
+ constructor(props) {
173
+ this.state = { bar: props.initialValue };
174
+ }
175
+ handleClick() {
176
+ this.setState({ bar: 'bar' });
177
+ }
178
+ render() {
179
+ return (
180
+ <Inner
181
+ name={this.state.bar}
182
+ onClick={this.handleClick}
183
+ />
184
+ );
185
+ }
186
+ }
187
+ test(<Foo initialValue="foo" />, 'DIV', 'foo');
188
+ expect(attachedListener).toThrow();
189
+ });
190
+
191
it('renders using forceUpdate even when there is no state', function() {
192
class Foo extends React.Component {
193
constructor(props) {
@@ -88,11 +211,62 @@ describe('ReactES6Class', function() {
211
expect(renderedName).toBe('bar');
212
});
213
214
+ it('will call all the normal life cycle methods', function() {
215
+ var lifeCycles = [];
216
+ class Foo {
217
+ constructor() {
218
+ this.state = {};
219
+ }
220
+ componentWillMount() {
221
+ lifeCycles.push('will-mount');
222
+ }
223
+ componentDidMount() {
224
+ lifeCycles.push('did-mount');
225
+ }
226
+ componentWillReceiveProps(nextProps) {
227
+ lifeCycles.push('receive-props', nextProps);
228
+ }
229
+ shouldComponentUpdate(nextProps, nextState) {
230
+ lifeCycles.push('should-update', nextProps, nextState);
231
+ return true;
232
+ }
233
+ componentWillUpdate(nextProps, nextState) {
234
+ lifeCycles.push('will-update', nextProps, nextState);
235
+ }
236
+ componentDidUpdate(prevProps, prevState) {
237
+ lifeCycles.push('did-update', prevProps, prevState);
238
+ }
239
+ componentWillUnmount() {
240
+ lifeCycles.push('will-unmount');
241
+ }
242
+ render() {
243
+ return <span className={this.props.value} />;
244
+ }
245
+ }
246
+ var instance = test(<Foo value="foo" />, 'SPAN', 'foo');
247
+ expect(lifeCycles).toEqual([
248
+ 'will-mount',
249
+ 'did-mount'
250
+ ]);
251
+ lifeCycles = []; // reset
252
+ test(<Foo value="bar" />, 'SPAN', 'bar');
253
+ expect(lifeCycles).toEqual([
254
+ 'receive-props', { value: 'bar' },
255
+ 'should-update', { value: 'bar' }, {},
256
+ 'will-update', { value: 'bar' }, {},
257
+ 'did-update', { value: 'foo' }, {}
258
+ ]);
259
+ lifeCycles = []; // reset
260
+ React.unmountComponentAtNode(container);
261
+ expect(lifeCycles).toEqual([
262
+ 'will-unmount'
263
+ ]);
264
+ });
265
+
266
it('warns when classic properties are defined on the instance, ' +
267
'but does not invoke them.', function() {
268
spyOn(console, 'warn');
269
var getInitialStateWasCalled = false;
95
- var componentWillMountWasCalled = false;
270
class Foo extends React.Component {
271
constructor() {
272
this.contextTypes = {};
@@ -102,27 +276,20 @@ describe('ReactES6Class', function() {
276
getInitialStateWasCalled = true;
277
return {};
278
}
105
- componentWillMount() {
106
- componentWillMountWasCalled = true;
107
- }
279
render() {
280
return <span className="foo" />;
281
}
282
}
283
test(<Foo />, 'SPAN', 'foo');
113
- // TODO: expect(getInitialStateWasCalled).toBe(false);
114
- // TODO: expect(componentWillMountWasCalled).toBe(false);
115
- expect(console.warn.calls.length).toBe(4);
284
+ expect(getInitialStateWasCalled).toBe(false);
285
+ expect(console.warn.calls.length).toBe(3);
286
expect(console.warn.calls[0].args[0]).toContain(
287
'getInitialState was defined on Foo, a plain JavaScript class.'
288
);
289
expect(console.warn.calls[1].args[0]).toContain(
120
- 'componentWillMount was defined on Foo, a plain JavaScript class.'
121
- );
122
- expect(console.warn.calls[2].args[0]).toContain(
290
'propTypes was defined as an instance property on Foo.'
291
);
125
- expect(console.warn.calls[3].args[0]).toContain(
292
+ expect(console.warn.calls[2].args[0]).toContain(
293
'contextTypes was defined as an instance property on Foo.'
294
);
295
});