Use Singleton LifeCycle State
All entry points are for reconciliation are within batching strategies. Since we don't have any batching strategies with synchronous updates, there can't be more than one life-cycle method on the stack at any given time. Therefore, it's safe to move the composite life cycle flag to a singleton. This saves us some memory management. I think that we can get rid of these life cycle states completely in the future.
Sebastian Markbage committed
Jan 27, 2015 at 01:19 UTC
bebc568ceb6d93dd5ded624842f9b7e9d9a84e8e
5 files changed
+115
-107
src/classic/class/ReactClass.js
+5
-3
@@ -15,6 +15,7 @@ var ReactComponentBase = require('ReactComponentBase');
15
var ReactElement = require('ReactElement');
16
var ReactErrorUtils = require('ReactErrorUtils');
17
var ReactInstanceMap = require('ReactInstanceMap');
18
+var ReactLifeCycle = require('ReactLifeCycle');
19
var ReactPropTypeLocations = require('ReactPropTypeLocations');
20
var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
21
var ReactUpdateQueue = require('ReactUpdateQueue');
@@ -734,9 +735,10 @@ var ReactClassMixin = {
735
*/
736
isMounted: function() {
737
var internalInstance = ReactInstanceMap.get(this);
737
- // In theory, isMounted is always true if it exists in the map.
738
- // TODO: Remove the internal isMounted method.
739
- return internalInstance && internalInstance.isMounted();
738
+ return (
739
+ internalInstance &&
740
+ internalInstance !== ReactLifeCycle.currentlyMountingInstance
741
+ );
742
},
743
744
/**
src/core/ReactCompositeComponent.js
+47
-31
@@ -42,6 +42,33 @@ function getDeclarationErrorAddendum(component) {
42
return '';
43
}
44
45
+/**
46
+ * ------------------ The Life-Cycle of a Composite Component ------------------
47
+ *
48
+ * - constructor: Initialization of state. The instance is now retained.
49
+ * - componentWillMount
50
+ * - render
51
+ * - [children's constructors]
52
+ * - [children's componentWillMount and render]
53
+ * - [children's componentDidMount]
54
+ * - componentDidMount
55
+ *
56
+ * Update Phases:
57
+ * - componentWillReceiveProps (only called if parent updated)
58
+ * - shouldComponentUpdate
59
+ * - componentWillUpdate
60
+ * - render
61
+ * - [children's constructors or receive props phases]
62
+ * - componentDidUpdate
63
+ *
64
+ * - componentWillUnmount
65
+ * - [children's componentWillUnmount]
66
+ * - [children destroyed]
67
+ * - (destroyed): The instance is now blank, released by React and ready for GC.
68
+ *
69
+ * -----------------------------------------------------------------------------
70
+ */
71
+
72
/**
73
* An incrementing ID assigned to each component when it is mounted. This is
74
* used to enforce the order in which `ReactUpdates` updates dirty components.
@@ -71,7 +98,6 @@ var ReactCompositeComponentMixin = {
98
this._pendingElement = null;
99
this._pendingState = null;
100
this._pendingForceUpdate = false;
74
- this._compositeLifeCycleState = null;
101
102
this._renderedComponent = null;
103
@@ -83,16 +109,6 @@ var ReactCompositeComponentMixin = {
109
this._pendingCallbacks = null;
110
},
111
86
- /**
87
- * Checks whether or not this composite component is mounted.
88
- * @return {boolean} True if mounted, false otherwise.
89
- * @protected
90
- * @final
91
- */
92
- isMounted: function() {
93
- return this._compositeLifeCycleState !== ReactLifeCycle.MOUNTING;
94
- },
95
-
112
/**
113
* Initializes the component, renders markup, and registers event listeners.
114
*
@@ -127,8 +143,6 @@ var ReactCompositeComponentMixin = {
143
// Store a reference from the instance back to the internal representation
144
ReactInstanceMap.set(inst, this);
145
130
- this._compositeLifeCycleState = ReactLifeCycle.MOUNTING;
131
-
146
if (__DEV__) {
147
this._warnIfContextsDiffer(this._currentElement._context, context);
148
}
@@ -176,13 +190,18 @@ var ReactCompositeComponentMixin = {
190
'%s.state: must be set to an object or null',
191
this.getName() || 'ReactCompositeComponent'
192
);
179
- inst.state = initialState;
193
194
this._pendingState = null;
195
this._pendingForceUpdate = false;
196
197
if (inst.componentWillMount) {
185
- inst.componentWillMount();
198
+ var previouslyMounting = ReactLifeCycle.currentlyMountingInstance;
199
+ ReactLifeCycle.currentlyMountingInstance = this;
200
+ try {
201
+ inst.componentWillMount();
202
+ } finally {
203
+ ReactLifeCycle.currentlyMountingInstance = previouslyMounting;
204
+ }
205
// When mounting, calls to `setState` by `componentWillMount` will set
206
// `this._pendingState` without triggering a re-render.
207
if (this._pendingState) {
@@ -197,8 +216,6 @@ var ReactCompositeComponentMixin = {
216
this._currentElement.type // The wrapping type
217
);
218
200
- // Done with mounting, `setState` will now trigger UI changes.
201
- this._compositeLifeCycleState = null;
219
var markup = ReactReconciler.mountComponent(
220
this._renderedComponent,
221
rootID,
@@ -221,11 +238,15 @@ var ReactCompositeComponentMixin = {
238
unmountComponent: function() {
239
var inst = this._instance;
240
224
- this._compositeLifeCycleState = ReactLifeCycle.UNMOUNTING;
241
if (inst.componentWillUnmount) {
226
- inst.componentWillUnmount();
242
+ var previouslyUnmounting = ReactLifeCycle.currentlyUnmountingInstance;
243
+ ReactLifeCycle.currentlyUnmountingInstance = this;
244
+ try {
245
+ inst.componentWillUnmount();
246
+ } finally {
247
+ ReactLifeCycle.currentlyUnmountingInstance = previouslyUnmounting;
248
+ }
249
}
228
- this._compositeLifeCycleState = null;
250
251
ReactReconciler.unmountComponent(this._renderedComponent);
252
this._renderedComponent = null;
@@ -238,6 +259,8 @@ var ReactCompositeComponentMixin = {
259
260
ReactComponentEnvironment.unmountIDFromEnvironment(this._rootNodeID);
261
262
+ // These fields do not really need to be reset since this object is no
263
+ // longer accessible.
264
this._context = null;
265
this._rootNodeID = null;
266
@@ -432,14 +455,6 @@ var ReactCompositeComponentMixin = {
455
},
456
457
receiveComponent: function(nextElement, transaction, nextContext) {
435
- var compositeLifeCycleState = this._compositeLifeCycleState;
436
- // Do not trigger a state transition if we are in the middle of mounting or
437
- // receiving props because both of those will already be doing this.
438
- if (compositeLifeCycleState === ReactLifeCycle.MOUNTING ||
439
- compositeLifeCycleState === ReactLifeCycle.RECEIVING_PROPS) {
440
- return;
441
- }
442
-
458
var prevElement = this._currentElement;
459
var prevContext = this._context;
460
@@ -552,14 +567,15 @@ var ReactCompositeComponentMixin = {
567
}
568
}
569
555
- this._compositeLifeCycleState = ReactLifeCycle.RECEIVING_PROPS;
570
+ // An update here will schedule an update but immediately set
571
+ // _pendingState which will ensure that any state updates gets
572
+ // immediately reconciled instead of waiting for the next batch.
573
+
574
if (inst.componentWillReceiveProps) {
575
inst.componentWillReceiveProps(nextProps, nextContext);
576
}
577
}
578
561
- this._compositeLifeCycleState = null;
562
-
579
var nextState = this._pendingState || inst.state;
580
this._pendingState = null;
581
src/core/ReactLifeCycle.js
+16
-41
@@ -11,50 +11,25 @@
11
12
"use strict";
13
14
-var keyMirror = require('keyMirror');
15
-
14
/**
17
- * `ReactCompositeComponent` maintains an auxiliary life cycle state in
18
- * `this._compositeLifeCycleState` (which can be null).
19
- *
20
- * This is different from the life cycle state maintained by `ReactComponent`.
21
- * The following diagram shows how the states overlap in
22
- * time. There are times when the CompositeLifeCycle is null - at those times it
23
- * is only meaningful to look at ComponentLifeCycle alone.
15
+ * This module manages the bookkeeping when a component is in the process
16
+ * of being mounted or being unmounted. This is used as a way to enforce
17
+ * invariants (or warnings) when it is not recommended to call
18
+ * setState/forceUpdate.
19
*
25
- * Top Row: ReactComponent.ComponentLifeCycle
26
- * Low Row: ReactComponent.CompositeLifeCycle
20
+ * currentlyMountingInstance: During the construction phase, it is not possible
21
+ * to trigger an update since the instance is not fully mounted yet. However, we
22
+ * currently allow this as a convenience for mutating the initial state.
23
*
28
- * +-------+---------------------------------+--------+
29
- * | UN | MOUNTED | UN |
30
- * |MOUNTED| | MOUNTED|
31
- * +-------+---------------------------------+--------+
32
- * | ^--------+ +-------+ +--------^ |
33
- * | | | | | | | |
34
- * | 0--|MOUNTING|-0-|RECEIVE|-0-| UN |--->0 |
35
- * | | | |PROPS | |MOUNTING| |
36
- * | | | | | | | |
37
- * | | | | | | | |
38
- * | +--------+ +-------+ +--------+ |
39
- * | | | |
40
- * +-------+---------------------------------+--------+
24
+ * currentlyUnmountingInstance: During the unmounting phase, the instance is
25
+ * still mounted and can therefore schedule an update. However, this is not
26
+ * recommended and probably an error since it's about to be unmounted.
27
+ * Therefore we still want to trigger in an error for that case.
28
*/
42
-var ReactLifeCycle = keyMirror({
43
- /**
44
- * Components in the process of being mounted respond to state changes
45
- * differently.
46
- */
47
- MOUNTING: null,
48
- /**
49
- * Components in the process of being unmounted are guarded against state
50
- * changes.
51
- */
52
- UNMOUNTING: null,
53
- /**
54
- * Components that are mounted and receiving new props respond to state
55
- * changes differently.
56
- */
57
- RECEIVING_PROPS: null
58
-});
29
+
30
+var ReactLifeCycle = {
31
+ currentlyMountingInstance: null,
32
+ currentlyUnmountingInstance: null
33
+};
34
35
module.exports = ReactLifeCycle;
src/core/ReactUpdateQueue.js
+5
-5
@@ -21,7 +21,7 @@ var assign = require('Object.assign');
21
var invariant = require('invariant');
22
23
function enqueueUpdate(internalInstance) {
24
- if (internalInstance._compositeLifeCycleState !== ReactLifeCycle.MOUNTING) {
24
+ if (internalInstance !== ReactLifeCycle.currentlyMountingInstance) {
25
// If we're in a componentWillMount handler, don't enqueue a rerender
26
// because ReactUpdates assumes we're in a browser context (which is
27
// wrong for server rendering) and we're about to do a render anyway.
@@ -49,8 +49,7 @@ function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
49
callerName
50
);
51
invariant(
52
- internalInstance._compositeLifeCycleState !==
53
- ReactLifeCycle.UNMOUNTING,
52
+ internalInstance !== ReactLifeCycle.currentlyUnmountingInstance,
53
'%s(...): Cannot call %s() on an unmounting component.',
54
callerName,
55
callerName
@@ -84,7 +83,7 @@ var ReactUpdateQueue = {
83
internalInstance,
84
'Cannot enqueue a callback on an instance that is unmounted.'
85
);
87
- if (internalInstance._compositeLifeCycleState === ReactLifeCycle.MOUNTING) {
86
+ if (internalInstance === ReactLifeCycle.currentlyMountingInstance) {
87
// Ignore callbacks in componentWillMount. See enqueueUpdate.
88
return;
89
}
@@ -95,7 +94,8 @@ var ReactUpdateQueue = {
94
}
95
// TODO: The callback here is ignored when setState is called from
96
// componentWillMount. Either fix it or disallow doing so completely in
98
- // favor of getInitialState.
97
+ // favor of getInitialState. Alternatively, we can disallow
98
+ // componentWillMount during server-side rendering.
99
enqueueUpdate(internalInstance);
100
},
101
src/core/__tests__/ReactComponentLifeCycle-test.js
+42
-27
@@ -14,13 +14,9 @@
14
var keyMirror = require('keyMirror');
15
16
var React;
17
-var ReactTestUtils;
18
-var ReactCompositeComponent;
17
+var ReactLifeCycle;
18
var ReactInstanceMap;
20
-var CompositeComponentLifeCycle;
21
-
22
-var getCompositeLifeCycle;
23
-var getLifeCycleState;
19
+var ReactTestUtils;
20
21
var clone = function(o) {
22
return JSON.parse(JSON.stringify(o));
@@ -84,6 +80,44 @@ var ComponentLifeCycle = keyMirror({
80
UNMOUNTED: null
81
});
82
83
+/**
84
+ * Composite components can also be in one of these life cycles.
85
+ */
86
+var CompositeComponentLifeCycle = keyMirror({
87
+ /**
88
+ * Mounted components have a DOM node representation and are capable of
89
+ * receiving new props.
90
+ */
91
+ MOUNTING: null,
92
+ /**
93
+ * Unmounted components are inactive and cannot receive new props.
94
+ */
95
+ UNMOUNTING: null
96
+});
97
+
98
+function getCompositeLifeCycle(instance) {
99
+ var internalInstance = ReactInstanceMap.get(instance);
100
+ if (!internalInstance) {
101
+ return null;
102
+ }
103
+ if (ReactLifeCycle.currentlyMountingInstance === internalInstance) {
104
+ return CompositeComponentLifeCycle.MOUNTING;
105
+ }
106
+ if (ReactLifeCycle.currentlyUnmountingInstance === internalInstance) {
107
+ return CompositeComponentLifeCycle.UNMOUNTING;
108
+ }
109
+ return null;
110
+}
111
+
112
+function getLifeCycleState(instance) {
113
+ var internalInstance = ReactInstanceMap.get(instance);
114
+ // Once a component gets mounted, it has an internal instance, once it
115
+ // gets unmounted, it loses that internal instance.
116
+ return internalInstance ?
117
+ ComponentLifeCycle.MOUNTED :
118
+ ComponentLifeCycle.UNMOUNTED;
119
+}
120
+
121
/**
122
* TODO: We should make any setState calls fail in
123
* `getInitialState` and `componentWillMount`. They will usually fail
@@ -96,27 +130,8 @@ describe('ReactComponentLifeCycle', function() {
130
require('mock-modules').dumpCache();
131
React = require('React');
132
ReactTestUtils = require('ReactTestUtils');
99
- ReactCompositeComponent = require('ReactCompositeComponent');
100
- CompositeComponentLifeCycle = require('ReactLifeCycle');
101
-
133
+ ReactLifeCycle = require('ReactLifeCycle');
134
ReactInstanceMap = require('ReactInstanceMap');
103
-
104
- getCompositeLifeCycle = function(instance) {
105
- var internalInstance = ReactInstanceMap.get(instance);
106
- if (!internalInstance) {
107
- return null;
108
- }
109
- return internalInstance._compositeLifeCycleState;
110
- };
111
-
112
- getLifeCycleState = function(instance) {
113
- var internalInstance = ReactInstanceMap.get(instance);
114
- // Once a component gets mounted, it has an internal instance, once it
115
- // gets unmounted, it loses that internal instance.
116
- return internalInstance ?
117
- ComponentLifeCycle.MOUNTED :
118
- ComponentLifeCycle.UNMOUNTED;
119
- };
135
});
136
137
it('should not reuse an instance when it has been unmounted', function() {
@@ -345,7 +360,7 @@ describe('ReactComponentLifeCycle', function() {
360
ComponentLifeCycle.MOUNTED
361
);
362
expect(instance._testJournal.compositeLifeCycleInInitialRender).toBe(
348
- CompositeComponentLifeCycle.MOUNTING
363
+ null
364
);
365
366
expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.MOUNTED);