@samitouri / QOS-React / commits / dd390b3e36

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