Skip to content

Commit

Permalink
Errors in componentWillUnmount should be caught by error boundary on …
Browse files Browse the repository at this point in the history
…initial render.
  • Loading branch information
jim committed Feb 12, 2016
1 parent bd39799 commit dd390b3
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 21 deletions.
46 changes: 46 additions & 0 deletions src/core/__tests__/ReactErrorBoundaries-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,52 @@ describe('ReactErrorBoundaries', function() {
expect(EventPluginHub.putListener).not.toBeCalled();
});

it('will catch exceptions in componentWillUnmount', function() {
class ErrorBoundary extends React.Component {
constructor() {
super();
this.state = {error: false};
}

render() {
if (!this.state.error) {
return <div>{this.props.children}</div>;
}
return <div>Error has been caught</div>;
}

unstable_handleError() {
this.setState({error: true});
}
}

class BrokenRender extends React.Component {
render() {
throw new Error('Always broken.');
}
}

class BrokenUnmount extends React.Component {
render() {
return <div />;
}
componentWillUnmount() {
throw new Error('Always broken.');
}
}

var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<BrokenUnmount />
<BrokenRender />
<BrokenUnmount />
</ErrorBoundary>,
container
);
ReactDOM.unmountComponentAtNode(container);
});

it('expect uneventful render to succeed', function() {
class Boundary extends React.Component {
constructor(props) {
Expand Down
7 changes: 4 additions & 3 deletions src/renderers/dom/client/ReactMount.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ function batchedMountComponentIntoNode(
* @internal
* @see {ReactMount.unmountComponentAtNode}
*/
function unmountComponentFromNode(instance, container) {
ReactReconciler.unmountComponent(instance);
function unmountComponentFromNode(instance, container, safely) {
ReactReconciler.unmountComponent(instance, safely);

if (container.nodeType === DOC_NODE_TYPE) {
container = container.documentElement;
Expand Down Expand Up @@ -567,7 +567,8 @@ var ReactMount = {
ReactUpdates.batchedUpdates(
unmountComponentFromNode,
prevComponent,
container
container,
false
);
return true;
},
Expand Down
4 changes: 2 additions & 2 deletions src/renderers/dom/shared/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,7 @@ ReactDOMComponent.Mixin = {
*
* @internal
*/
unmountComponent: function() {
unmountComponent: function(safely) {
switch (this._tag) {
case 'iframe':
case 'img':
Expand Down Expand Up @@ -1043,7 +1043,7 @@ ReactDOMComponent.Mixin = {
break;
}

this.unmountChildren();
this.unmountChildren(safely);
ReactDOMComponentTree.uncacheNode(this);
EventPluginHub.deleteAllListeners(this);
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
Expand Down
8 changes: 4 additions & 4 deletions src/renderers/shared/reconciler/ReactChildReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ var ReactChildReconciler = {
} else {
if (prevChild) {
removedNodes[name] = ReactReconciler.getNativeNode(prevChild);
ReactReconciler.unmountComponent(prevChild);
ReactReconciler.unmountComponent(prevChild, false);
}
// The child must be instantiated before it's mounted.
var nextChildInstance = instantiateReactComponent(nextElement);
Expand All @@ -113,7 +113,7 @@ var ReactChildReconciler = {
!(nextChildren && nextChildren.hasOwnProperty(name))) {
prevChild = prevChildren[name];
removedNodes[name] = ReactReconciler.getNativeNode(prevChild);
ReactReconciler.unmountComponent(prevChild);
ReactReconciler.unmountComponent(prevChild, false);
}
}
},
Expand All @@ -125,11 +125,11 @@ var ReactChildReconciler = {
* @param {?object} renderedChildren Previously initialized set of children.
* @internal
*/
unmountChildren: function(renderedChildren) {
unmountChildren: function(renderedChildren, safely) {
for (var name in renderedChildren) {
if (renderedChildren.hasOwnProperty(name)) {
var renderedChild = renderedChildren[name];
ReactReconciler.unmountComponent(renderedChild);
ReactReconciler.unmountComponent(renderedChild, safely);
}
}
},
Expand Down
16 changes: 11 additions & 5 deletions src/renderers/shared/reconciler/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
var ReactComponentEnvironment = require('ReactComponentEnvironment');
var ReactCurrentOwner = require('ReactCurrentOwner');
var ReactElement = require('ReactElement');
var ReactErrorUtils = require('ReactErrorUtils');
var ReactInstanceMap = require('ReactInstanceMap');
var ReactNodeTypes = require('ReactNodeTypes');
var ReactPerf = require('ReactPerf');
Expand Down Expand Up @@ -316,7 +317,7 @@ var ReactCompositeComponentMixin = {
}
checkpoint = transaction.checkpoint();

this._renderedComponent.unmountComponent();
this._renderedComponent.unmountComponent(true);
transaction.rollback(checkpoint);

// Try again - we've informed the component about the error, so they can render an error message this time.
Expand Down Expand Up @@ -368,18 +369,23 @@ var ReactCompositeComponentMixin = {
* @final
* @internal
*/
unmountComponent: function() {
unmountComponent: function(safely) {
if (!this._renderedComponent) {
return;
}
var inst = this._instance;

if (inst.componentWillUnmount) {
inst.componentWillUnmount();
if (safely) {
var name = this.getName() + '.componentWillUnmount()';
ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
} else {
inst.componentWillUnmount();
}
}

if (this._renderedComponent) {
ReactReconciler.unmountComponent(this._renderedComponent);
ReactReconciler.unmountComponent(this._renderedComponent, safely);
this._renderedNodeType = null;
this._renderedComponent = null;
this._instance = null;
Expand Down Expand Up @@ -805,7 +811,7 @@ var ReactCompositeComponentMixin = {
);
} else {
var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance);
ReactReconciler.unmountComponent(prevComponentInstance, false);

this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
this._renderedComponent = this._instantiateReactComponent(
Expand Down
8 changes: 4 additions & 4 deletions src/renderers/shared/reconciler/ReactMultiChild.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ var ReactMultiChild = {
updateTextContent: function(nextContent) {
var prevChildren = this._renderedChildren;
// Remove any rendered children.
ReactChildReconciler.unmountChildren(prevChildren);
ReactChildReconciler.unmountChildren(prevChildren, false);
for (var name in prevChildren) {
if (prevChildren.hasOwnProperty(name)) {
invariant(false, 'updateTextContent called on non-empty component.');
Expand All @@ -262,7 +262,7 @@ var ReactMultiChild = {
updateMarkup: function(nextMarkup) {
var prevChildren = this._renderedChildren;
// Remove any rendered children.
ReactChildReconciler.unmountChildren(prevChildren);
ReactChildReconciler.unmountChildren(prevChildren, false);
for (var name in prevChildren) {
if (prevChildren.hasOwnProperty(name)) {
invariant(false, 'updateTextContent called on non-empty component.');
Expand Down Expand Up @@ -366,9 +366,9 @@ var ReactMultiChild = {
*
* @internal
*/
unmountChildren: function() {
unmountChildren: function(safely) {
var renderedChildren = this._renderedChildren;
ReactChildReconciler.unmountChildren(renderedChildren);
ReactChildReconciler.unmountChildren(renderedChildren, safely);
this._renderedChildren = null;
},

Expand Down
4 changes: 2 additions & 2 deletions src/renderers/shared/reconciler/ReactReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ var ReactReconciler = {
* @final
* @internal
*/
unmountComponent: function(internalInstance) {
unmountComponent: function(internalInstance, safely) {
ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
return internalInstance.unmountComponent();
return internalInstance.unmountComponent(safely);
},

/**
Expand Down
2 changes: 1 addition & 1 deletion src/test/ReactTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ ReactShallowRenderer.prototype.getRenderOutput = function() {

ReactShallowRenderer.prototype.unmount = function() {
if (this._instance) {
this._instance.unmountComponent();
this._instance.unmountComponent(false);
}
};

Expand Down

0 comments on commit dd390b3

Please sign in to comment.