Warn when passing invalid containers to render and unmountComponentAtNode

This commit is contained in:
Charles Marsh
2015-09-03 17:00:11 -07:00
committed by Ben Alpert
parent 4139b2e223
commit 270a805369
3 changed files with 117 additions and 10 deletions
+50 -2
View File
@@ -587,8 +587,16 @@ var ReactMount = {
var reactRootElement = getReactRootElementInContainer(container);
var containerHasReactMarkup =
reactRootElement && ReactMount.isRenderedByReact(reactRootElement);
var containerHasNonRootReactChild =
ReactMount.hasNonRootReactChild(container);
if (__DEV__) {
warning(
!containerHasNonRootReactChild,
'renderComponent(...): Replacing React-rendered children with a new ' +
'root component.'
);
if (!containerHasReactMarkup || reactRootElement.nextSibling) {
var rootElementSibling = reactRootElement;
while (rootElementSibling) {
@@ -601,13 +609,15 @@ var ReactMount = {
);
break;
}
rootElementSibling = rootElementSibling.nextSibling;
}
}
}
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent;
var shouldReuseMarkup =
containerHasReactMarkup &&
!prevComponent &&
!containerHasNonRootReactChild;
var component = ReactMount._renderNewRootComponent(
nextWrappedElement,
container,
@@ -697,6 +707,28 @@ var ReactMount = {
var reactRootID = getReactRootID(container);
var component = instancesByReactRootID[reactRootID];
if (!component) {
// Check if the node being unmounted was rendered by React, but isn't a
// root node.
var containerHasNonRootReactChild = ReactMount.hasNonRootReactChild(
container);
// Check if the container itself is a React root node.
var containerID = ReactMount.getID(container);
var containerRootID = ReactInstanceHandles.getReactRootIDFromNodeID(
containerID);
var isContainerReactRoot =
containerID && containerRootID && containerID === containerRootID;
if (__DEV__) {
warning(
!containerHasNonRootReactChild,
'unmountComponentAtNode(): The node you\'re attempting to unmount ' +
'is not a valid React root node, and thus cannot be unmounted.%s',
(isContainerReactRoot ? ' You may have passed in a React root ' +
'node as argument, rather than its container.' : '')
);
}
return false;
}
ReactUpdates.batchedUpdates(
@@ -781,6 +813,22 @@ var ReactMount = {
return id ? id.charAt(0) === SEPARATOR : false;
},
/**
* True if the supplied DOM node has a direct React-rendered child that is
* not a React root element. Useful for warning in renderComponent`,
* `unmountComponentAtNode`, etc.
*
* @param {?DOMElement} node The candidate DOM node.
* @return {boolean} True if the DOM element contains a direct child that was
* rendered by React but is not a root element.
* @internal
*/
hasNonRootReactChild: function(node) {
var reactRootID = getReactRootID(node);
return reactRootID ? reactRootID !==
ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID) : false;
},
/**
* Traverses up the ancestors of the supplied node to find a node that is a
* DOM representation of a React component.
@@ -224,4 +224,24 @@ describe('ReactMount', function() {
expect(console.error.mock.calls.length).toBe(1);
expect(console.error.mock.calls[0][0]).toContain('two copies of React');
});
it('should warn if render removes React-rendered children', function() {
var container = document.createElement('container');
var Component = React.createClass({
render: function() {
return <div><div /></div>;
},
});
React.render(<Component />, container);
// Test that blasting away children throws a warning
spyOn(console, 'error');
var rootNode = container.firstChild;
React.render(<span />, rootNode);
expect(console.error.callCount).toBe(1);
expect(console.error.mostRecentCall.args[0]).toBe(
'Warning: renderComponent(...): Replacing React-rendered children ' +
'with a new root component.'
);
});
});
@@ -19,18 +19,12 @@ describe('ReactMount', function() {
var mainContainerDiv = document.createElement('div');
document.body.appendChild(mainContainerDiv);
var instanceOne = (
<div className="firstReactDiv">
</div>
);
var instanceOne = <div className="firstReactDiv" />;
var firstRootDiv = document.createElement('div');
mainContainerDiv.appendChild(firstRootDiv);
ReactDOM.render(instanceOne, firstRootDiv);
var instanceTwo = (
<div className="secondReactDiv">
</div>
);
var instanceTwo = <div className="secondReactDiv" />;
var secondRootDiv = document.createElement('div');
mainContainerDiv.appendChild(secondRootDiv);
ReactDOM.render(instanceTwo, secondRootDiv);
@@ -45,4 +39,49 @@ describe('ReactMount', function() {
ReactDOM.unmountComponentAtNode(secondRootDiv);
expect(secondRootDiv.firstChild).toBeNull();
});
it('should warn when unmounting a non-container root node', function() {
var mainContainerDiv = document.createElement('div');
var component =
<div>
<div />
</div>;
ReactDOM.render(component, mainContainerDiv);
// Test that unmounting at a root node gives a helpful warning
var rootDiv = mainContainerDiv.firstChild;
spyOn(console, 'error');
ReactDOM.unmountComponentAtNode(rootDiv);
expect(console.error.callCount).toBe(1);
expect(console.error.mostRecentCall.args[0]).toBe(
'Warning: unmountComponentAtNode(): The node you\'re attempting to ' +
'unmount is not a valid React root node, and thus cannot be ' +
'unmounted. You may have passed in a React root node as argument, ' +
'rather than its container.'
);
});
it('should warn when unmounting a non-container, non-root node', function() {
var mainContainerDiv = document.createElement('div');
var component =
<div>
<div>
<div />
</div>
</div>;
ReactDOM.render(component, mainContainerDiv);
// Test that unmounting at a non-root node gives a different warning
var nonRootDiv = mainContainerDiv.firstChild.firstChild;
spyOn(console, 'error');
ReactDOM.unmountComponentAtNode(nonRootDiv);
expect(console.error.callCount).toBe(1);
expect(console.error.mostRecentCall.args[0]).toBe(
'Warning: unmountComponentAtNode(): The node you\'re attempting to ' +
'unmount is not a valid React root node, and thus cannot be ' +
'unmounted.'
);
});
});