Fetch node to unmount separately from unmounting

My last strategy of getting each node recursively while unmounting was a pain to make work properly with ReactMount's confusing cache. Now, we get the node before unmounting anything in the subtree (and we don't try to find the nodes of descendants).

This is a temporary solution and can go away when we get rid of the giant ReactMount node hash map.

Fixes #5151.
This commit is contained in:
Ben Alpert
2015-10-13 12:07:57 -07:00
parent 50c9b94552
commit 622db4ee4f
8 changed files with 79 additions and 19 deletions
@@ -245,4 +245,34 @@ describe('ReactMount', function() {
'render the new components instead of calling ReactDOM.render.'
);
});
it('should not crash in node cache when unmounting', function() {
var Component = React.createClass({
render: function() {
// Add refs to some nodes so that they get traversed and cached
return (
<div ref="a">
<div ref="b">b</div>
{this.props.showC && <div>c</div>}
</div>
);
},
});
var container = document.createElement('container');
ReactDOM.render(<div><Component showC={false} /></div>, container);
// Right now, A and B are in the cache. When we add C, it won't get added to
// the cache (assuming markup-string mode).
ReactDOM.render(<div><Component showC={true} /></div>, container);
// Remove A, B, and C. Unmounting C shouldn't cause B to get recached.
ReactDOM.render(<div></div>, container);
// Add them back -- this shouldn't cause a cached node collision.
ReactDOM.render(<div><Component showC={true} /></div>, container);
ReactDOM.unmountComponentAtNode(container);
});
});
@@ -1068,6 +1068,10 @@ ReactDOMComponent.Mixin = {
}
},
getNativeNode: function() {
return getNode(this);
},
/**
* Destroys all event registrations for this instance. Does not remove from
* the DOM. That must be done by the parent.
@@ -1112,19 +1116,16 @@ ReactDOMComponent.Mixin = {
break;
}
var nativeNode = getNode(this);
this._nativeNode = null;
if (this._nodeHasLegacyProperties) {
nativeNode._reactInternalComponent = null;
this._nativeNode._reactInternalComponent = null;
}
this._nativeNode = null;
this.unmountChildren();
ReactBrowserEventEmitter.deleteAllListeners(this._rootNodeID);
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
this._rootNodeID = null;
this._wrapperState = null;
return nativeNode;
},
getPublicInstance: function() {
@@ -23,6 +23,14 @@ var escapeTextContentForBrowser = require('escapeTextContentForBrowser');
var setTextContent = require('setTextContent');
var validateDOMNesting = require('validateDOMNesting');
function getNode(inst) {
if (inst._nativeNode) {
return inst._nativeNode;
} else {
return inst._nativeNode = ReactMount.getNode(inst._rootNodeID);
}
}
/**
* Text nodes violate a couple assumptions that React makes about components:
*
@@ -133,20 +141,18 @@ assign(ReactDOMTextComponent.prototype, {
// and/or updateComponent to do the actual update for consistency with
// other component types?
this._stringText = nextStringText;
var node = this._nativeNode;
if (!node) {
node = this._nativeNode = ReactMount.getNode(this._rootNodeID);
}
DOMChildrenOperations.updateTextContent(node, nextStringText);
DOMChildrenOperations.updateTextContent(getNode(this), nextStringText);
}
}
},
getNativeNode: function() {
return getNode(this);
},
unmountComponent: function() {
var node = this._nativeNode || ReactMount.getNode(this._rootNodeID);
this._nativeNode = null;
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
return node;
},
});
@@ -309,6 +309,10 @@ var ReactCompositeComponentMixin = {
return markup;
},
getNativeNode: function() {
return ReactReconciler.getNativeNode(this._renderedComponent);
},
/**
* Releases any resources allocated by `mountComponent`.
*
@@ -322,8 +326,7 @@ var ReactCompositeComponentMixin = {
inst.componentWillUnmount();
}
var unmountedNativeNode =
ReactReconciler.unmountComponent(this._renderedComponent);
ReactReconciler.unmountComponent(this._renderedComponent);
this._renderedComponent = null;
this._instance = null;
@@ -352,7 +355,6 @@ var ReactCompositeComponentMixin = {
// TODO: inst.props = null;
// TODO: inst.state = null;
// TODO: inst.context = null;
return unmountedNativeNode;
},
/**
@@ -741,8 +743,14 @@ var ReactCompositeComponentMixin = {
this._processChildContext(context)
);
} else {
var oldNativeNode =
ReactReconciler.unmountComponent(prevComponentInstance);
// TODO: This is currently necessary due to the unfortunate caching
// that ReactMount does which makes it exceedingly difficult to unmount
// a set of siblings without accidentally repopulating the node cache (see
// #5151). Once ReactMount no longer stores the nodes by ID, this method
// can go away.
var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance);
this._renderedComponent = this._instantiateReactComponent(
nextRenderedElement
@@ -53,12 +53,14 @@ assign(ReactEmptyComponent.prototype, {
},
receiveComponent: function() {
},
getNativeNode: function() {
return ReactReconciler.getNativeNode(this._renderedComponent);
},
unmountComponent: function(rootID, transaction, context) {
var nativeNode = ReactReconciler.unmountComponent(this._renderedComponent);
ReactReconciler.unmountComponent(this._renderedComponent);
ReactEmptyComponentRegistry.deregisterNullComponentID(this._rootNodeID);
this._rootNodeID = null;
this._renderedComponent = null;
return nativeNode;
},
});
@@ -57,6 +57,14 @@ var ReactReconciler = {
return markup;
},
/**
* Returns a value that can be passed to
* ReactComponentEnvironment.replaceNodeWithMarkup.
*/
getNativeNode: function(internalInstance) {
return internalInstance.getNativeNode();
},
/**
* Releases any resources allocated by `mountComponent`.
*
@@ -105,6 +105,7 @@ function instantiateReactComponent(node) {
typeof instance.construct === 'function' &&
typeof instance.mountComponent === 'function' &&
typeof instance.receiveComponent === 'function' &&
typeof instance.getNativeNode === 'function' &&
typeof instance.unmountComponent === 'function',
'Only React Components can be mounted.'
);
+4
View File
@@ -386,6 +386,10 @@ NoopInternalComponent.prototype = {
this._currentElement = element;
},
getNativeNode: function() {
return undefined;
},
unmountComponent: function() {
},