Errors in componentWillUnmount should be caught by error boundary on initial render.
jim committed
Feb 8, 2016 at 16:21 UTC
dd390b3e36e63f528173eb75cc2e8350d3209de9
8 files changed
+74
-21
src/core/__tests__/ReactErrorBoundaries-test.js
+46
@@ -55,6 +55,52 @@ describe('ReactErrorBoundaries', function() {
55
expect(EventPluginHub.putListener).not.toBeCalled();
56
});
57
58
+ it('will catch exceptions in componentWillUnmount', function() {
59
+ class ErrorBoundary extends React.Component {
60
+ constructor() {
61
+ super();
62
+ this.state = {error: false};
63
+ }
64
+
65
+ render() {
66
+ if (!this.state.error) {
67
+ return <div>{this.props.children}</div>;
68
+ }
69
+ return <div>Error has been caught</div>;
70
+ }
71
+
72
+ unstable_handleError() {
73
+ this.setState({error: true});
74
+ }
75
+ }
76
+
77
+ class BrokenRender extends React.Component {
78
+ render() {
79
+ throw new Error('Always broken.');
80
+ }
81
+ }
82
+
83
+ class BrokenUnmount extends React.Component {
84
+ render() {
85
+ return <div />;
86
+ }
87
+ componentWillUnmount() {
88
+ throw new Error('Always broken.');
89
+ }
90
+ }
91
+
92
+ var container = document.createElement('div');
93
+ ReactDOM.render(
94
+ <ErrorBoundary>
95
+ <BrokenUnmount />
96
+ <BrokenRender />
97
+ <BrokenUnmount />
98
+ </ErrorBoundary>,
99
+ container
100
+ );
101
+ ReactDOM.unmountComponentAtNode(container);
102
+ });
103
+
104
it('expect uneventful render to succeed', function() {
105
class Boundary extends React.Component {
106
constructor(props) {
src/renderers/dom/client/ReactMount.js
+4
-3
@@ -168,8 +168,8 @@ function batchedMountComponentIntoNode(
168
* @internal
169
* @see {ReactMount.unmountComponentAtNode}
170
*/
171
-function unmountComponentFromNode(instance, container) {
172
- ReactReconciler.unmountComponent(instance);
171
+function unmountComponentFromNode(instance, container, safely) {
172
+ ReactReconciler.unmountComponent(instance, safely);
173
174
if (container.nodeType === DOC_NODE_TYPE) {
175
container = container.documentElement;
@@ -567,7 +567,8 @@ var ReactMount = {
567
ReactUpdates.batchedUpdates(
568
unmountComponentFromNode,
569
prevComponent,
570
- container
570
+ container,
571
+ false
572
);
573
return true;
574
},
src/renderers/dom/shared/ReactDOMComponent.js
+2
-2
@@ -1008,7 +1008,7 @@ ReactDOMComponent.Mixin = {
1008
*
1009
* @internal
1010
*/
1011
- unmountComponent: function() {
1011
+ unmountComponent: function(safely) {
1012
switch (this._tag) {
1013
case 'iframe':
1014
case 'img':
@@ -1043,7 +1043,7 @@ ReactDOMComponent.Mixin = {
1043
break;
1044
}
1045
1046
- this.unmountChildren();
1046
+ this.unmountChildren(safely);
1047
ReactDOMComponentTree.uncacheNode(this);
1048
EventPluginHub.deleteAllListeners(this);
1049
ReactComponentBrowserEnvironment.unmountIDFromEnvironment(this._rootNodeID);
src/renderers/shared/reconciler/ReactChildReconciler.js
+4
-4
@@ -100,7 +100,7 @@ var ReactChildReconciler = {
100
} else {
101
if (prevChild) {
102
removedNodes[name] = ReactReconciler.getNativeNode(prevChild);
103
- ReactReconciler.unmountComponent(prevChild);
103
+ ReactReconciler.unmountComponent(prevChild, false);
104
}
105
// The child must be instantiated before it's mounted.
106
var nextChildInstance = instantiateReactComponent(nextElement);
@@ -113,7 +113,7 @@ var ReactChildReconciler = {
113
!(nextChildren && nextChildren.hasOwnProperty(name))) {
114
prevChild = prevChildren[name];
115
removedNodes[name] = ReactReconciler.getNativeNode(prevChild);
116
- ReactReconciler.unmountComponent(prevChild);
116
+ ReactReconciler.unmountComponent(prevChild, false);
117
}
118
}
119
},
@@ -125,11 +125,11 @@ var ReactChildReconciler = {
125
* @param {?object} renderedChildren Previously initialized set of children.
126
* @internal
127
*/
128
- unmountChildren: function(renderedChildren) {
128
+ unmountChildren: function(renderedChildren, safely) {
129
for (var name in renderedChildren) {
130
if (renderedChildren.hasOwnProperty(name)) {
131
var renderedChild = renderedChildren[name];
132
- ReactReconciler.unmountComponent(renderedChild);
132
+ ReactReconciler.unmountComponent(renderedChild, safely);
133
}
134
}
135
},
src/renderers/shared/reconciler/ReactCompositeComponent.js
+11
-5
@@ -14,6 +14,7 @@
14
var ReactComponentEnvironment = require('ReactComponentEnvironment');
15
var ReactCurrentOwner = require('ReactCurrentOwner');
16
var ReactElement = require('ReactElement');
17
+var ReactErrorUtils = require('ReactErrorUtils');
18
var ReactInstanceMap = require('ReactInstanceMap');
19
var ReactNodeTypes = require('ReactNodeTypes');
20
var ReactPerf = require('ReactPerf');
@@ -316,7 +317,7 @@ var ReactCompositeComponentMixin = {
317
}
318
checkpoint = transaction.checkpoint();
319
319
- this._renderedComponent.unmountComponent();
320
+ this._renderedComponent.unmountComponent(true);
321
transaction.rollback(checkpoint);
322
323
// Try again - we've informed the component about the error, so they can render an error message this time.
@@ -368,18 +369,23 @@ var ReactCompositeComponentMixin = {
369
* @final
370
* @internal
371
*/
371
- unmountComponent: function() {
372
+ unmountComponent: function(safely) {
373
if (!this._renderedComponent) {
374
return;
375
}
376
var inst = this._instance;
377
378
if (inst.componentWillUnmount) {
378
- inst.componentWillUnmount();
379
+ if (safely) {
380
+ var name = this.getName() + '.componentWillUnmount()';
381
+ ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
382
+ } else {
383
+ inst.componentWillUnmount();
384
+ }
385
}
386
387
if (this._renderedComponent) {
382
- ReactReconciler.unmountComponent(this._renderedComponent);
388
+ ReactReconciler.unmountComponent(this._renderedComponent, safely);
389
this._renderedNodeType = null;
390
this._renderedComponent = null;
391
this._instance = null;
@@ -805,7 +811,7 @@ var ReactCompositeComponentMixin = {
811
);
812
} else {
813
var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);
808
- ReactReconciler.unmountComponent(prevComponentInstance);
814
+ ReactReconciler.unmountComponent(prevComponentInstance, false);
815
816
this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);
817
this._renderedComponent = this._instantiateReactComponent(
src/renderers/shared/reconciler/ReactMultiChild.js
+4
-4
@@ -242,7 +242,7 @@ var ReactMultiChild = {
242
updateTextContent: function(nextContent) {
243
var prevChildren = this._renderedChildren;
244
// Remove any rendered children.
245
- ReactChildReconciler.unmountChildren(prevChildren);
245
+ ReactChildReconciler.unmountChildren(prevChildren, false);
246
for (var name in prevChildren) {
247
if (prevChildren.hasOwnProperty(name)) {
248
invariant(false, 'updateTextContent called on non-empty component.');
@@ -262,7 +262,7 @@ var ReactMultiChild = {
262
updateMarkup: function(nextMarkup) {
263
var prevChildren = this._renderedChildren;
264
// Remove any rendered children.
265
- ReactChildReconciler.unmountChildren(prevChildren);
265
+ ReactChildReconciler.unmountChildren(prevChildren, false);
266
for (var name in prevChildren) {
267
if (prevChildren.hasOwnProperty(name)) {
268
invariant(false, 'updateTextContent called on non-empty component.');
@@ -366,9 +366,9 @@ var ReactMultiChild = {
366
*
367
* @internal
368
*/
369
- unmountChildren: function() {
369
+ unmountChildren: function(safely) {
370
var renderedChildren = this._renderedChildren;
371
- ReactChildReconciler.unmountChildren(renderedChildren);
371
+ ReactChildReconciler.unmountChildren(renderedChildren, safely);
372
this._renderedChildren = null;
373
},
374
src/renderers/shared/reconciler/ReactReconciler.js
+2
-2
@@ -68,9 +68,9 @@ var ReactReconciler = {
68
* @final
69
* @internal
70
*/
71
- unmountComponent: function(internalInstance) {
71
+ unmountComponent: function(internalInstance, safely) {
72
ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
73
- return internalInstance.unmountComponent();
73
+ return internalInstance.unmountComponent(safely);
74
},
75
76
/**
src/test/ReactTestUtils.js
+1
-1
@@ -448,7 +448,7 @@ ReactShallowRenderer.prototype.getRenderOutput = function() {
448
449
ReactShallowRenderer.prototype.unmount = function() {
450
if (this._instance) {
451
- this._instance.unmountComponent();
451
+ this._instance.unmountComponent(false);
452
}
453
};
454