Skip to content

Commit dac59d1

Browse files
committed
Drop the _owner and _lifeCycle field on internal instances
The _owner field is unnecessary since it's reachable from _currentElement. The _lifeCycle field is unnecessary because an internal component should not even need to exist at all if it's unmounted. It should be dereferenced internally, and never exposed externally. The only case where it's important is for batching updates where we currently avoid calling performUpdateIfNecessary if it's mounted. However, this function is already only executed "if necessary" so we just make sure that it's not necessary after unmount by resetting all the pending fields.
1 parent 03ae0a9 commit dac59d1

File tree

6 files changed

+93
-104
lines changed

6 files changed

+93
-104
lines changed

src/core/ReactComponent.js

Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,6 @@ var assign = require('Object.assign');
2020
var invariant = require('invariant');
2121
var keyMirror = require('keyMirror');
2222

23-
/**
24-
* Every React component is in one of these life cycles.
25-
*/
26-
var ComponentLifeCycle = keyMirror({
27-
/**
28-
* Mounted components have a DOM node representation and are capable of
29-
* receiving new props.
30-
*/
31-
MOUNTED: null,
32-
/**
33-
* Unmounted components are inactive and cannot receive new props.
34-
*/
35-
UNMOUNTED: null
36-
});
37-
3823
var injected = false;
3924

4025
/**
@@ -115,11 +100,6 @@ var ReactComponent = {
115100
}
116101
},
117102

118-
/**
119-
* @internal
120-
*/
121-
LifeCycle: ComponentLifeCycle,
122-
123103
/**
124104
* Injected module that provides ability to mutate individual properties.
125105
* Injected into the base class because many different subclasses need access
@@ -137,17 +117,6 @@ var ReactComponent = {
137117
*/
138118
Mixin: {
139119

140-
/**
141-
* Checks whether or not this component is mounted.
142-
*
143-
* @return {boolean} True if mounted, false otherwise.
144-
* @final
145-
* @protected
146-
*/
147-
isMounted: function() {
148-
return this._lifeCycleState === ComponentLifeCycle.MOUNTED;
149-
},
150-
151120
/**
152121
* Sets a subset of the props.
153122
*
@@ -175,10 +144,6 @@ var ReactComponent = {
175144
* @public
176145
*/
177146
replaceProps: function(props, callback) {
178-
invariant(
179-
this.isMounted(),
180-
'replaceProps(...): Can only update a mounted component.'
181-
);
182147
invariant(
183148
this._mountDepth === 0,
184149
'replaceProps(...): You called `setProps` or `replaceProps` on a ' +
@@ -225,15 +190,6 @@ var ReactComponent = {
225190
* @internal
226191
*/
227192
construct: function(element) {
228-
// Record the component responsible for creating this component.
229-
// This is accessible through the element but we maintain an extra
230-
// field for compatibility with devtools and as a way to make an
231-
// incremental update. TODO: Consider deprecating this field.
232-
this._owner = element._owner;
233-
234-
// All components start unmounted.
235-
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
236-
237193
// See ReactUpdates.
238194
this._pendingCallbacks = null;
239195

@@ -258,20 +214,12 @@ var ReactComponent = {
258214
* @internal
259215
*/
260216
mountComponent: function(rootID, transaction, mountDepth) {
261-
invariant(
262-
!this.isMounted(),
263-
'mountComponent(%s, ...): Can only mount an unmounted component. ' +
264-
'Make sure to avoid storing components between renders or reusing a ' +
265-
'single component instance in multiple places.',
266-
rootID
267-
);
268217
var ref = this._currentElement.ref;
269218
if (ref != null) {
270219
var owner = this._currentElement._owner;
271220
attachRef(ref, this, owner);
272221
}
273222
this._rootNodeID = rootID;
274-
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
275223
this._mountDepth = mountDepth;
276224
// Effectively: return '';
277225
},
@@ -287,17 +235,15 @@ var ReactComponent = {
287235
* @internal
288236
*/
289237
unmountComponent: function() {
290-
invariant(
291-
this.isMounted(),
292-
'unmountComponent(): Can only unmount a mounted component.'
293-
);
294238
var ref = this._currentElement.ref;
295239
if (ref != null) {
296-
detachRef(ref, this, this._owner);
240+
detachRef(ref, this, this._currentElement._owner);
297241
}
298242
unmountIDFromEnvironment(this._rootNodeID);
243+
// Reset all fields
299244
this._rootNodeID = null;
300-
this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
245+
this._pendingCallbacks = null;
246+
this._pendingElement = null;
301247
},
302248

303249
/**
@@ -312,10 +258,6 @@ var ReactComponent = {
312258
* @internal
313259
*/
314260
receiveComponent: function(nextElement, transaction) {
315-
invariant(
316-
this.isMounted(),
317-
'receiveComponent(...): Can only update a mounted component.'
318-
);
319261
this._pendingElement = nextElement;
320262
this.performUpdateIfNecessary(transaction);
321263
},
@@ -333,7 +275,6 @@ var ReactComponent = {
333275
var prevElement = this._currentElement;
334276
var nextElement = this._pendingElement;
335277
this._currentElement = nextElement;
336-
this._owner = nextElement._owner;
337278
this._pendingElement = null;
338279
this.updateComponent(transaction, prevElement, nextElement);
339280
},

src/core/ReactCompositeComponent.js

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
2828
var warning = require('warning');
2929

3030
function getDeclarationErrorAddendum(component) {
31-
var owner = component._owner || null;
31+
var owner = component._currentElement._owner || null;
3232
if (owner) {
3333
var constructor = owner._instance.constructor;
3434
if (constructor && constructor.displayName) {
@@ -56,8 +56,8 @@ function validateLifeCycleOnReplaceState(instance) {
5656
* `ReactCompositeComponent` maintains an auxiliary life cycle state in
5757
* `this._compositeLifeCycleState` (which can be null).
5858
*
59-
* This is different from the life cycle state maintained by `ReactComponent` in
60-
* `this._lifeCycleState`. The following diagram shows how the states overlap in
59+
* This is different from the life cycle state maintained by `ReactComponent`.
60+
* The following diagram shows how the states overlap in
6161
* time. There are times when the CompositeLifeCycle is null - at those times it
6262
* is only meaningful to look at ComponentLifeCycle alone.
6363
*
@@ -129,8 +129,7 @@ var ReactCompositeComponentMixin = assign({},
129129
* @final
130130
*/
131131
isMounted: function() {
132-
return ReactComponent.Mixin.isMounted.call(this) &&
133-
this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING;
132+
return this._compositeLifeCycleState !== CompositeLifeCycle.MOUNTING;
134133
},
135134

136135
/**
@@ -232,6 +231,9 @@ var ReactCompositeComponentMixin = assign({},
232231
this._renderedComponent.unmountComponent();
233232
this._renderedComponent = null;
234233

234+
// Reset pending fields
235+
this._pendingState = null;
236+
this._pendingForceUpdate = false;
235237
ReactComponent.Mixin.unmountComponent.call(this);
236238

237239
// Delete the reference from the instance to this internal representation
@@ -554,11 +556,6 @@ var ReactCompositeComponentMixin = assign({},
554556
inst.props = nextProps;
555557
inst.state = nextState;
556558
inst.context = nextContext;
557-
558-
// Owner cannot change because shouldUpdateReactComponent doesn't allow
559-
// it. TODO: Remove this._owner completely.
560-
this._owner = nextParentElement._owner;
561-
562559
return;
563560
}
564561

@@ -606,10 +603,6 @@ var ReactCompositeComponentMixin = assign({},
606603
inst.state = nextState;
607604
inst.context = nextContext;
608605

609-
// Owner cannot change because shouldUpdateReactComponent doesn't allow
610-
// it. TODO: Remove this._owner completely.
611-
this._owner = nextElement._owner;
612-
613606
this._updateRenderedComponent(transaction);
614607

615608
if (inst.componentDidUpdate) {

src/core/ReactUpdates.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -136,24 +136,23 @@ function runBatchedUpdates(transaction) {
136136
dirtyComponents.sort(mountDepthComparator);
137137

138138
for (var i = 0; i < len; i++) {
139-
// If a component is unmounted before pending changes apply, ignore them
140-
// TODO: Queue unmounts in the same list to avoid this happening at all
139+
// If a component is unmounted before pending changes apply, it will still
140+
// be here, but we assume that it has cleared its _pendingCallbacks and
141+
// that performUpdateIfNecessary is a noop.
141142
var component = dirtyComponents[i];
142-
if (component.isMounted()) {
143-
// If performUpdateIfNecessary happens to enqueue any new updates, we
144-
// shouldn't execute the callbacks until the next render happens, so
145-
// stash the callbacks first
146-
var callbacks = component._pendingCallbacks;
147-
component._pendingCallbacks = null;
148-
component.performUpdateIfNecessary(transaction.reconcileTransaction);
149-
150-
if (callbacks) {
151-
for (var j = 0; j < callbacks.length; j++) {
152-
transaction.callbackQueue.enqueue(
153-
callbacks[j],
154-
component
155-
);
156-
}
143+
// If performUpdateIfNecessary happens to enqueue any new updates, we
144+
// shouldn't execute the callbacks until the next render happens, so
145+
// stash the callbacks first
146+
var callbacks = component._pendingCallbacks;
147+
component._pendingCallbacks = null;
148+
component.performUpdateIfNecessary(transaction.reconcileTransaction);
149+
150+
if (callbacks) {
151+
for (var j = 0; j < callbacks.length; j++) {
152+
transaction.callbackQueue.enqueue(
153+
callbacks[j],
154+
component
155+
);
157156
}
158157
}
159158
}

src/core/__tests__/ReactComponentLifeCycle-test.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212
"use strict";
1313

14+
var keyMirror = require('keyMirror');
15+
1416
var React;
1517
var ReactTestUtils;
1618
var ReactComponent;
1719
var ReactCompositeComponent;
1820
var ReactInstanceMap;
19-
var ComponentLifeCycle;
2021
var CompositeComponentLifeCycle;
2122

2223
var getCompositeLifeCycle;
@@ -69,6 +70,21 @@ var POST_WILL_UNMOUNT_STATE = {
6970
hasWillUnmountCompleted: true
7071
};
7172

73+
/**
74+
* Every React component is in one of these life cycles.
75+
*/
76+
var ComponentLifeCycle = keyMirror({
77+
/**
78+
* Mounted components have a DOM node representation and are capable of
79+
* receiving new props.
80+
*/
81+
MOUNTED: null,
82+
/**
83+
* Unmounted components are inactive and cannot receive new props.
84+
*/
85+
UNMOUNTED: null
86+
});
87+
7288
/**
7389
* TODO: We should make any setState calls fail in
7490
* `getInitialState` and `componentWillMount`. They will usually fail
@@ -83,7 +99,6 @@ describe('ReactComponentLifeCycle', function() {
8399
ReactTestUtils = require('ReactTestUtils');
84100
ReactComponent = require('ReactComponent');
85101
ReactCompositeComponent = require('ReactCompositeComponent');
86-
ComponentLifeCycle = ReactComponent.LifeCycle;
87102
CompositeComponentLifeCycle = ReactCompositeComponent.LifeCycle;
88103

89104
ReactInstanceMap = require('ReactInstanceMap');
@@ -94,13 +109,11 @@ describe('ReactComponentLifeCycle', function() {
94109

95110
getLifeCycleState = function(instance) {
96111
var internalInstance = ReactInstanceMap.get(instance);
97-
if (!internalInstance) {
98-
// Once a component is fully unmounted, we cannot actually get to the
99-
// internal instance. It's already dereferenced and possibly GC:ed.
100-
// So the unmounted life cycle hook doesn't exist anymore.
101-
return ComponentLifeCycle.UNMOUNTED;
102-
}
103-
return internalInstance._lifeCycleState;
112+
// Once a component gets mounted, it has an internal instance, once it
113+
// gets unmounted, it loses that internal instance.
114+
return internalInstance ?
115+
ComponentLifeCycle.MOUNTED :
116+
ComponentLifeCycle.UNMOUNTED;
104117
};
105118
});
106119

src/core/__tests__/ReactUpdates-test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,4 +815,46 @@ describe('ReactUpdates', function() {
815815
'asap-1.2'
816816
]);
817817
});
818+
819+
it('does not call render after a component as been deleted', function() {
820+
821+
var renderCount = 0;
822+
var componentB = null;
823+
824+
var B = React.createClass({
825+
getInitialState: function() {
826+
return { updates: 0 };
827+
},
828+
componentDidMount: function() {
829+
componentB = this;
830+
},
831+
render: function() {
832+
renderCount++;
833+
return <div />;
834+
}
835+
});
836+
837+
var A = React.createClass({
838+
getInitialState: function() {
839+
return { showB: true };
840+
},
841+
render: function() {
842+
return this.state.showB ? <B /> : <div />;
843+
}
844+
});
845+
846+
var container = document.createElement('div');
847+
848+
var component = React.render(<A />, container);
849+
850+
ReactUpdates.batchedUpdates(function() {
851+
// B will have scheduled an update but the batching should ensure that its
852+
// update never fires.
853+
componentB.setState({ updates: 1 });
854+
component.setState({ showB: false });
855+
});
856+
857+
expect(renderCount).toBe(1);
858+
});
859+
818860
});

src/test/ReactDefaultPerf.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ var ReactDefaultPerf = {
244244

245245
entry.displayNames[rootNodeID] = {
246246
current: this.constructor.displayName,
247-
owner: this._owner ? this._owner.constructor.displayName : '<root>'
247+
owner: this._currentElement._owner ?
248+
this._currentElement._owner.constructor.displayName : '<root>'
248249
};
249250

250251
return rv;

0 commit comments

Comments
 (0)