diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js
index c0e3c9ec00e2e..4c9a77f8c7dbe 100644
--- a/src/renderers/dom/client/ReactMount.js
+++ b/src/renderers/dom/client/ReactMount.js
@@ -170,11 +170,15 @@ function batchedMountComponentIntoNode(
* @internal
* @see {ReactMount.unmountComponentAtNode}
*/
-function unmountComponentFromNode(instance, container, safely) {
+function unmountComponentFromNode(instance, container) {
if (__DEV__) {
ReactInstrumentation.debugTool.onBeginFlush();
}
- ReactReconciler.unmountComponent(instance, safely);
+ ReactReconciler.unmountComponent(
+ instance,
+ false /* safely */,
+ false /* skipLifecycle */
+ );
if (__DEV__) {
ReactInstrumentation.debugTool.onEndFlush();
}
@@ -618,8 +622,7 @@ var ReactMount = {
ReactUpdates.batchedUpdates(
unmountComponentFromNode,
prevComponent,
- container,
- false
+ container
);
return true;
},
diff --git a/src/renderers/dom/shared/ReactDOMComponent.js b/src/renderers/dom/shared/ReactDOMComponent.js
index ef75df0c42b8b..0da6954928477 100644
--- a/src/renderers/dom/shared/ReactDOMComponent.js
+++ b/src/renderers/dom/shared/ReactDOMComponent.js
@@ -1155,7 +1155,7 @@ ReactDOMComponent.Mixin = {
*
* @internal
*/
- unmountComponent: function(safely) {
+ unmountComponent: function(safely, skipLifecycle) {
switch (this._tag) {
case 'audio':
case 'form':
@@ -1197,7 +1197,7 @@ ReactDOMComponent.Mixin = {
break;
}
- this.unmountChildren(safely);
+ this.unmountChildren(safely, skipLifecycle);
ReactDOMComponentTree.uncacheNode(this);
EventPluginHub.deleteAllListeners(this);
this._rootNodeID = 0;
diff --git a/src/renderers/native/ReactNativeBaseComponent.js b/src/renderers/native/ReactNativeBaseComponent.js
index 2192b019fc4ab..1d47e9dbdfde5 100644
--- a/src/renderers/native/ReactNativeBaseComponent.js
+++ b/src/renderers/native/ReactNativeBaseComponent.js
@@ -55,10 +55,10 @@ ReactNativeBaseComponent.Mixin = {
return this;
},
- unmountComponent: function() {
+ unmountComponent: function(safely, skipLifecycle) {
ReactNativeComponentTree.uncacheNode(this);
deleteAllListeners(this);
- this.unmountChildren();
+ this.unmountChildren(safely, skipLifecycle);
this._rootNodeID = 0;
},
diff --git a/src/renderers/shared/stack/reconciler/ReactChildReconciler.js b/src/renderers/shared/stack/reconciler/ReactChildReconciler.js
index d487e70999f04..dc58ff648e06d 100644
--- a/src/renderers/shared/stack/reconciler/ReactChildReconciler.js
+++ b/src/renderers/shared/stack/reconciler/ReactChildReconciler.js
@@ -146,7 +146,11 @@ var ReactChildReconciler = {
} else {
if (prevChild) {
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
- ReactReconciler.unmountComponent(prevChild, false);
+ ReactReconciler.unmountComponent(
+ prevChild,
+ false, /* safely */
+ false /* skipLifecycle */
+ );
}
// The child must be instantiated before it's mounted.
var nextChildInstance = instantiateReactComponent(nextElement, true);
@@ -170,7 +174,11 @@ var ReactChildReconciler = {
!(nextChildren && nextChildren.hasOwnProperty(name))) {
prevChild = prevChildren[name];
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
- ReactReconciler.unmountComponent(prevChild, false);
+ ReactReconciler.unmountComponent(
+ prevChild,
+ false, /* safely */
+ false /* skipLifecycle */
+ );
}
}
},
@@ -182,11 +190,11 @@ var ReactChildReconciler = {
* @param {?object} renderedChildren Previously initialized set of children.
* @internal
*/
- unmountChildren: function(renderedChildren, safely) {
+ unmountChildren: function(renderedChildren, safely, skipLifecycle) {
for (var name in renderedChildren) {
if (renderedChildren.hasOwnProperty(name)) {
var renderedChild = renderedChildren[name];
- ReactReconciler.unmountComponent(renderedChild, safely);
+ ReactReconciler.unmountComponent(renderedChild, safely, skipLifecycle);
}
}
},
diff --git a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
index 56869d0ae6987..3c49fc464df14 100644
--- a/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
+++ b/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
@@ -333,6 +333,23 @@ var ReactCompositeComponent = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
+ if (inst.componentWillMount) {
+ if (__DEV__) {
+ measureLifeCyclePerf(
+ () => inst.componentWillMount(),
+ this._debugID,
+ 'componentWillMount'
+ );
+ } else {
+ inst.componentWillMount();
+ }
+ // When mounting, calls to `setState` by `componentWillMount` will set
+ // `this._pendingStateQueue` without triggering a re-render.
+ if (this._pendingStateQueue) {
+ inst.state = this._processPendingState(inst.props, inst.context);
+ }
+ }
+
var markup;
if (inst.unstable_handleError) {
markup = this.performInitialMountWithErrorHandling(
@@ -343,7 +360,13 @@ var ReactCompositeComponent = {
context
);
} else {
- markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
+ markup = this.performInitialMount(
+ renderedElement,
+ hostParent,
+ hostContainerInfo,
+ transaction,
+ context
+ );
}
if (inst.componentDidMount) {
@@ -434,7 +457,13 @@ var ReactCompositeComponent = {
var markup;
var checkpoint = transaction.checkpoint();
try {
- markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
+ markup = this.performInitialMount(
+ renderedElement,
+ hostParent,
+ hostContainerInfo,
+ transaction,
+ context
+ );
} catch (e) {
// Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint
transaction.rollback(checkpoint);
@@ -443,42 +472,33 @@ var ReactCompositeComponent = {
this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
}
checkpoint = transaction.checkpoint();
-
- this._renderedComponent.unmountComponent(true);
+ this._renderedComponent.unmountComponent(
+ true, /* safely */
+ // Don't call componentWillUnmount() because they never fully mounted:
+ true /* skipLifecyle */
+ );
transaction.rollback(checkpoint);
// Try again - we've informed the component about the error, so they can render an error message this time.
// If this throws again, the error will bubble up (and can be caught by a higher error boundary).
- markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context);
+ markup = this.performInitialMount(
+ renderedElement,
+ hostParent,
+ hostContainerInfo,
+ transaction,
+ context
+ );
}
return markup;
},
- performInitialMount: function(renderedElement, hostParent, hostContainerInfo, transaction, context) {
- var inst = this._instance;
-
- var debugID = 0;
- if (__DEV__) {
- debugID = this._debugID;
- }
-
- if (inst.componentWillMount) {
- if (__DEV__) {
- measureLifeCyclePerf(
- () => inst.componentWillMount(),
- debugID,
- 'componentWillMount'
- );
- } else {
- inst.componentWillMount();
- }
- // When mounting, calls to `setState` by `componentWillMount` will set
- // `this._pendingStateQueue` without triggering a re-render.
- if (this._pendingStateQueue) {
- inst.state = this._processPendingState(inst.props, inst.context);
- }
- }
-
+ performInitialMount: function(
+ renderedElement,
+ hostParent,
+ hostContainerInfo,
+ transaction,
+ context
+ ) {
// If not a stateless component, we now render
if (renderedElement === undefined) {
renderedElement = this._renderValidatedComponent();
@@ -492,6 +512,11 @@ var ReactCompositeComponent = {
);
this._renderedComponent = child;
+ var debugID = 0;
+ if (__DEV__) {
+ debugID = this._debugID;
+ }
+
var markup = ReactReconciler.mountComponent(
child,
transaction,
@@ -521,7 +546,7 @@ var ReactCompositeComponent = {
* @final
* @internal
*/
- unmountComponent: function(safely) {
+ unmountComponent: function(safely, skipLifecycle) {
if (!this._renderedComponent) {
return;
}
@@ -532,8 +557,10 @@ var ReactCompositeComponent = {
inst._calledComponentWillUnmount = true;
if (safely) {
- var name = this.getName() + '.componentWillUnmount()';
- ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
+ if (!skipLifecycle) {
+ var name = this.getName() + '.componentWillUnmount()';
+ ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
+ }
} else {
if (__DEV__) {
measureLifeCyclePerf(
@@ -548,7 +575,11 @@ var ReactCompositeComponent = {
}
if (this._renderedComponent) {
- ReactReconciler.unmountComponent(this._renderedComponent, safely);
+ ReactReconciler.unmountComponent(
+ this._renderedComponent,
+ safely,
+ skipLifecycle
+ );
this._renderedNodeType = null;
this._renderedComponent = null;
this._instance = null;
@@ -941,7 +972,11 @@ var ReactCompositeComponent = {
inst.state = nextState;
inst.context = nextContext;
- this._updateRenderedComponent(transaction, unmaskedContext);
+ if (inst.unstable_handleError) {
+ this._updateRenderedComponentWithErrorHandling(transaction, unmaskedContext);
+ } else {
+ this._updateRenderedComponent(transaction, unmaskedContext);
+ }
if (hasComponentDidUpdate) {
if (__DEV__) {
@@ -961,6 +996,40 @@ var ReactCompositeComponent = {
}
},
+ /**
+ * Call the component's `render` method and update the DOM accordingly.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ _updateRenderedComponentWithErrorHandling: function(transaction, context) {
+ var checkpoint = transaction.checkpoint();
+ try {
+ this._updateRenderedComponent(transaction, context);
+ } catch (e) {
+ // Roll back to checkpoint, handle error (which may add items to the transaction),
+ // and take a new checkpoint
+ transaction.rollback(checkpoint);
+ this._instance.unstable_handleError(e);
+ if (this._pendingStateQueue) {
+ this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
+ }
+ checkpoint = transaction.checkpoint();
+
+ // Gracefully update to a clean state
+ this._updateRenderedComponentWithNextElement(
+ transaction,
+ context,
+ null,
+ true /* safely */
+ );
+
+ // Try again - we've informed the component about the error, so they can render an error message this time.
+ // If this throws again, the error will bubble up (and can be caught by a higher error boundary).
+ this._updateRenderedComponent(transaction, context);
+ }
+ },
+
/**
* Call the component's `render` method and update the DOM accordingly.
*
@@ -968,9 +1037,29 @@ var ReactCompositeComponent = {
* @internal
*/
_updateRenderedComponent: function(transaction, context) {
+ var nextRenderedElement = this._renderValidatedComponent();
+ this._updateRenderedComponentWithNextElement(
+ transaction,
+ context,
+ nextRenderedElement,
+ false /* safely */
+ );
+ },
+
+ /**
+ * Call the component's `render` method and update the DOM accordingly.
+ *
+ * @param {ReactReconcileTransaction} transaction
+ * @internal
+ */
+ _updateRenderedComponentWithNextElement: function(
+ transaction,
+ context,
+ nextRenderedElement,
+ safely
+ ) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
- var nextRenderedElement = this._renderValidatedComponent();
var debugID = 0;
if (__DEV__) {
@@ -986,7 +1075,11 @@ var ReactCompositeComponent = {
);
} else {
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
- ReactReconciler.unmountComponent(prevComponentInstance, false);
+ ReactReconciler.unmountComponent(
+ prevComponentInstance,
+ safely,
+ false /* skipLifecycle */
+ );
var nodeType = ReactNodeTypes.getType(nextRenderedElement);
this._renderedNodeType = nodeType;
diff --git a/src/renderers/shared/stack/reconciler/ReactMultiChild.js b/src/renderers/shared/stack/reconciler/ReactMultiChild.js
index 220de49f3e6b5..18fa1e8d0bd94 100644
--- a/src/renderers/shared/stack/reconciler/ReactMultiChild.js
+++ b/src/renderers/shared/stack/reconciler/ReactMultiChild.js
@@ -289,7 +289,11 @@ var ReactMultiChild = {
updateTextContent: function(nextContent) {
var prevChildren = this._renderedChildren;
// Remove any rendered children.
- ReactChildReconciler.unmountChildren(prevChildren, false);
+ ReactChildReconciler.unmountChildren(
+ prevChildren,
+ false, /* safely */
+ false /* skipLifecycle */
+ );
for (var name in prevChildren) {
if (prevChildren.hasOwnProperty(name)) {
invariant(false, 'updateTextContent called on non-empty component.');
@@ -309,7 +313,11 @@ var ReactMultiChild = {
updateMarkup: function(nextMarkup) {
var prevChildren = this._renderedChildren;
// Remove any rendered children.
- ReactChildReconciler.unmountChildren(prevChildren, false);
+ ReactChildReconciler.unmountChildren(
+ prevChildren,
+ false, /* safely */
+ false /* skipLifecycle */
+ );
for (var name in prevChildren) {
if (prevChildren.hasOwnProperty(name)) {
invariant(false, 'updateTextContent called on non-empty component.');
@@ -423,9 +431,13 @@ var ReactMultiChild = {
*
* @internal
*/
- unmountChildren: function(safely) {
+ unmountChildren: function(safely, skipLifecycle) {
var renderedChildren = this._renderedChildren;
- ReactChildReconciler.unmountChildren(renderedChildren, safely);
+ ReactChildReconciler.unmountChildren(
+ renderedChildren,
+ safely,
+ skipLifecycle
+ );
this._renderedChildren = null;
},
diff --git a/src/renderers/shared/stack/reconciler/ReactReconciler.js b/src/renderers/shared/stack/reconciler/ReactReconciler.js
index 3e5f1658ca8cf..f55bf339ee7aa 100644
--- a/src/renderers/shared/stack/reconciler/ReactReconciler.js
+++ b/src/renderers/shared/stack/reconciler/ReactReconciler.js
@@ -93,7 +93,7 @@ var ReactReconciler = {
* @final
* @internal
*/
- unmountComponent: function(internalInstance, safely) {
+ unmountComponent: function(internalInstance, safely, skipLifecycle) {
if (__DEV__) {
if (internalInstance._debugID !== 0) {
ReactInstrumentation.debugTool.onBeforeUnmountComponent(
@@ -102,7 +102,7 @@ var ReactReconciler = {
}
}
ReactRef.detachRefs(internalInstance, internalInstance._currentElement);
- internalInstance.unmountComponent(safely);
+ internalInstance.unmountComponent(safely, skipLifecycle);
if (__DEV__) {
if (internalInstance._debugID !== 0) {
ReactInstrumentation.debugTool.onUnmountComponent(
diff --git a/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js b/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js
index a81793eab56c9..dc3c82077f4ff 100644
--- a/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js
+++ b/src/renderers/shared/stack/reconciler/ReactSimpleEmptyComponent.js
@@ -40,8 +40,12 @@ Object.assign(ReactSimpleEmptyComponent.prototype, {
getHostNode: function() {
return ReactReconciler.getHostNode(this._renderedComponent);
},
- unmountComponent: function() {
- ReactReconciler.unmountComponent(this._renderedComponent);
+ unmountComponent: function(safely, skipLifecycle) {
+ ReactReconciler.unmountComponent(
+ this._renderedComponent,
+ safely,
+ skipLifecycle
+ );
this._renderedComponent = null;
},
});
diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
index 75c8654e9b5ec..ac5e410cad911 100644
--- a/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
+++ b/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js
@@ -15,291 +15,1492 @@ var React;
var ReactDOM;
describe('ReactErrorBoundaries', () => {
+ var log;
+
+ var BrokenConstructor;
+ var BrokenComponentWillMount;
+ var BrokenComponentDidMount;
+ var BrokenComponentWillReceiveProps;
+ var BrokenComponentWillUpdate;
+ var BrokenComponentDidUpdate;
+ var BrokenComponentWillUnmount;
+ var BrokenRenderErrorBoundary;
+ var BrokenComponentWillMountErrorBoundary;
+ var BrokenRender;
+ var ErrorBoundary;
+ var ErrorMessage;
+ var NoopErrorBoundary;
+ var Normal;
beforeEach(() => {
ReactDOM = require('ReactDOM');
React = require('React');
- });
- it('does not register event handlers for unmounted children', () => {
- class Angry extends React.Component {
+ log = [];
+
+ BrokenConstructor = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenConstructor constructor [!]');
+ throw new Error('Hello');
+ }
render() {
- throw new Error('Please, do not render me.');
+ log.push('BrokenConstructor render');
+ return
;
}
- }
+ componentWillMount() {
+ log.push('BrokenConstructor componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenConstructor componentDidMount');
+ }
+ componentWillReceiveProps() {
+ log.push('BrokenConstructor componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenConstructor componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenConstructor componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenConstructor componentWillUnmount');
+ }
+ };
- class Boundary extends React.Component {
+ BrokenComponentWillMount = class extends React.Component {
constructor(props) {
super(props);
- this.state = {error: false};
+ log.push('BrokenComponentWillMount constructor');
}
render() {
- if (!this.state.error) {
- return (
-
- );
- } else {
- return Happy Birthday!
;
- }
+ log.push('BrokenComponentWillMount render');
+ return ;
}
- onClick() {
- /* do nothing */
+ componentWillMount() {
+ log.push('BrokenComponentWillMount componentWillMount [!]');
+ throw new Error('Hello');
}
- unstable_handleError() {
- this.setState({error: true});
+ componentDidMount() {
+ log.push('BrokenComponentWillMount componentDidMount');
}
- }
-
- var EventPluginHub = require('EventPluginHub');
- var container = document.createElement('div');
- EventPluginHub.putListener = jest.fn();
- ReactDOM.render(, container);
- expect(EventPluginHub.putListener).not.toBeCalled();
- });
+ componentWillReceiveProps() {
+ log.push('BrokenComponentWillMount componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenComponentWillMount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillMount componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillMount componentWillUnmount');
+ }
+ };
- it('renders an error state', () => {
- var log = [];
- class Angry extends React.Component {
+ BrokenComponentDidMount = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentDidMount constructor');
+ }
render() {
- log.push('Angry render');
- throw new Error('Please, do not render me.');
+ log.push('BrokenComponentDidMount render');
+ return ;
+ }
+ componentWillMount() {
+ log.push('BrokenComponentDidMount componentWillMount');
}
componentDidMount() {
- log.push('Angry componentDidMount');
+ log.push('BrokenComponentDidMount componentDidMount [!]');
+ throw new Error('Hello');
+ }
+ componentWillReceiveProps() {
+ log.push('BrokenComponentDidMount componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenComponentDidMount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentDidMount componentDidUpdate');
}
componentWillUnmount() {
- log.push('Angry componentWillUnmount');
+ log.push('BrokenComponentDidMount componentWillUnmount');
}
- }
+ };
- class Boundary extends React.Component {
+ BrokenComponentWillReceiveProps = class extends React.Component {
constructor(props) {
super(props);
- this.state = {error: false};
+ log.push('BrokenComponentWillReceiveProps constructor');
}
render() {
- log.push('Boundary render');
- if (!this.state.error) {
- return (
-
- );
- } else {
- return Happy Birthday!
;
- }
+ log.push('BrokenComponentWillReceiveProps render');
+ return ;
+ }
+ componentWillMount() {
+ log.push('BrokenComponentWillReceiveProps componentWillMount');
}
componentDidMount() {
- log.push('Boundary componentDidMount');
+ log.push('BrokenComponentWillReceiveProps componentDidMount');
}
- componentWillUnmount() {
- log.push('Boundary componentWillUnmount');
+ componentWillReceiveProps() {
+ log.push('BrokenComponentWillReceiveProps componentWillReceiveProps [!]');
+ throw new Error('Hello');
}
- onClick() {
- /* do nothing */
+ componentWillUpdate() {
+ log.push('BrokenComponentWillReceiveProps componentWillUpdate');
}
- unstable_handleError() {
- this.setState({error: true});
+ componentDidUpdate() {
+ log.push('BrokenComponentWillReceiveProps componentDidUpdate');
}
- }
-
- var container = document.createElement('div');
- ReactDOM.render(, container);
- expect(container.firstChild.innerHTML).toBe('Happy Birthday!');
- expect(log).toEqual([
- 'Boundary render',
- 'Angry render',
- 'Boundary render',
- 'Boundary componentDidMount',
- ]);
- });
-
- it('will catch exceptions in componentWillUnmount', () => {
- class ErrorBoundary extends React.Component {
- constructor() {
- super();
- this.state = {error: false};
+ componentWillUnmount() {
+ log.push('BrokenComponentWillReceiveProps componentWillUnmount');
}
+ };
+ BrokenComponentWillUpdate = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillUpdate constructor');
+ }
render() {
- if (!this.state.error) {
- return {this.props.children}
;
- }
- return Error has been caught
;
+ log.push('BrokenComponentWillUpdate render');
+ return ;
}
-
- unstable_handleError() {
- this.setState({error: true});
+ componentWillMount() {
+ log.push('BrokenComponentWillUpdate componentWillMount');
}
- }
+ componentDidMount() {
+ log.push('BrokenComponentWillUpdate componentDidMount');
+ }
+ componentWillReceiveProps() {
+ log.push('BrokenComponentWillUpdate componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenComponentWillUpdate componentWillUpdate [!]');
+ throw new Error('Hello');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillUpdate componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentWillUpdate componentWillUnmount');
+ }
+ };
- class BrokenRender extends React.Component {
+ BrokenComponentDidUpdate = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentDidUpdate constructor');
+ }
render() {
- throw new Error('Always broken.');
+ log.push('BrokenComponentDidUpdate render');
+ return ;
}
- }
+ componentWillMount() {
+ log.push('BrokenComponentDidUpdate componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentDidUpdate componentDidMount');
+ }
+ componentWillReceiveProps() {
+ log.push('BrokenComponentDidUpdate componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenComponentDidUpdate componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentDidUpdate componentDidUpdate [!]');
+ throw new Error('Hello');
+ }
+ componentWillUnmount() {
+ log.push('BrokenComponentDidUpdate componentWillUnmount');
+ }
+ };
- class BrokenUnmount extends React.Component {
+ BrokenComponentWillUnmount = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenComponentWillUnmount constructor');
+ }
render() {
+ log.push('BrokenComponentWillUnmount render');
return ;
}
+ componentWillMount() {
+ log.push('BrokenComponentWillUnmount componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenComponentWillUnmount componentDidMount');
+ }
+ componentWillReceiveProps() {
+ log.push('BrokenComponentWillUnmount componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenComponentWillUnmount componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenComponentWillUnmount componentDidUpdate');
+ }
componentWillUnmount() {
- throw new Error('Always broken.');
+ log.push('BrokenComponentWillUnmount componentWillUnmount [!]');
+ throw new Error('Hello');
}
- }
-
- var container = document.createElement('div');
- ReactDOM.render(
-
-
-
-
- ,
- container
- );
- ReactDOM.unmountComponentAtNode(container);
- });
+ };
- it('expect uneventful render to succeed', () => {
- var log = [];
- class Boundary extends React.Component {
+ BrokenComponentWillMountErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
- this.state = {error: false};
+ this.state = {error: null};
+ log.push('BrokenComponentWillMountErrorBoundary constructor');
}
render() {
- log.push('Boundary render');
- return ;
+ if (this.state.error) {
+ log.push('BrokenComponentWillMountErrorBoundary render error');
+ return Caught an error: {this.state.error.message}.
;
+ }
+ log.push('BrokenComponentWillMountErrorBoundary render success');
+ return {this.props.children}
;
}
- onClick() {
- /* do nothing */
+ componentWillMount() {
+ log.push('BrokenComponentWillMountErrorBoundary componentWillMount [!]');
+ throw new Error('Hello');
}
componentDidMount() {
- log.push('Boundary componentDidMount');
+ log.push('BrokenComponentWillMountErrorBoundary componentDidMount');
}
componentWillUnmount() {
- log.push('Boundary componentWillUnmount');
+ log.push('BrokenComponentWillMountErrorBoundary componentWillUnmount');
}
- unstable_handleError() {
- this.setState({error: true});
+ unstable_handleError(error) {
+ log.push('BrokenComponentWillMountErrorBoundary unstable_handleError');
+ this.setState({error});
}
- }
-
- var container = document.createElement('div');
- ReactDOM.render(, container);
- expect(log).toEqual([
- 'Boundary render',
- 'Boundary componentDidMount',
- ]);
- });
+ };
- it('correctly handles composite siblings', () => {
- class ErrorBoundary extends React.Component {
- constructor() {
- super();
- this.state = {error: false};
+ BrokenRenderErrorBoundary = class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {error: null};
+ log.push('BrokenRenderErrorBoundary constructor');
}
-
render() {
- if (!this.state.error) {
- return {this.props.children}
;
+ if (this.state.error) {
+ log.push('BrokenRenderErrorBoundary render error [!]');
+ throw new Error('Hello');
}
- return Error has been caught
;
+ log.push('BrokenRenderErrorBoundary render success');
+ return {this.props.children}
;
}
-
- unstable_handleError() {
- this.setState({error: true});
+ componentWillMount() {
+ log.push('BrokenRenderErrorBoundary componentWillMount');
}
- }
-
- function Broken() {
- throw new Error('Always broken.');
- }
-
- function Composite() {
- return ;
- }
-
- var container = document.createElement('div');
- ReactDOM.render(
- ,
- container
- );
- ReactDOM.unmountComponentAtNode(container);
- });
+ componentDidMount() {
+ log.push('BrokenRenderErrorBoundary componentDidMount');
+ }
+ componentWillUnmount() {
+ log.push('BrokenRenderErrorBoundary componentWillUnmount');
+ }
+ unstable_handleError(error) {
+ log.push('BrokenRenderErrorBoundary unstable_handleError');
+ this.setState({error});
+ }
+ };
- it('catches errors from children', () => {
- var log = [];
+ BrokenRender = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('BrokenRender constructor');
+ }
+ render() {
+ log.push('BrokenRender render [!]');
+ throw new Error('Hello');
+ }
+ componentWillMount() {
+ log.push('BrokenRender componentWillMount');
+ }
+ componentDidMount() {
+ log.push('BrokenRender componentDidMount');
+ }
+ componentWillReceiveProps() {
+ log.push('BrokenRender componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('BrokenRender componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('BrokenRender componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('BrokenRender componentWillUnmount');
+ }
+ };
- class Box extends React.Component {
+ NoopErrorBoundary = class extends React.Component {
constructor(props) {
super(props);
- this.state = {errorMessage: null};
+ log.push('NoopErrorBoundary constructor');
}
render() {
- if (this.state.errorMessage != null) {
- log.push('Box renderError');
- return Error: {this.state.errorMessage}
;
- }
- log.push('Box render');
- var ref = function(x) {
- log.push('Inquisitive ref ' + x);
- };
- return (
-
- );
+ log.push('NoopErrorBoundary render');
+ return ;
}
- unstable_handleError(e) {
- this.setState({errorMessage: e.message});
+ componentWillMount() {
+ log.push('NoopErrorBoundary componentWillMount');
}
componentDidMount() {
- log.push('Box componentDidMount');
+ log.push('NoopErrorBoundary componentDidMount');
}
componentWillUnmount() {
- log.push('Box componentWillUnmount');
+ log.push('NoopErrorBoundary componentWillUnmount');
}
- }
+ unstable_handleError() {
+ log.push('NoopErrorBoundary unstable_handleError');
+ }
+ };
- class Inquisitive extends React.Component {
+ Normal = class extends React.Component {
+ static defaultProps = {
+ logName: 'Normal',
+ };
+ constructor(props) {
+ super(props);
+ log.push(`${this.props.logName} constructor`);
+ }
render() {
- log.push('Inquisitive render');
- return What is love?
;
+ log.push(`${this.props.logName} render`);
+ return {this.props.children}
;
+ }
+ componentWillMount() {
+ log.push(`${this.props.logName} componentWillMount`);
}
componentDidMount() {
- log.push('Inquisitive componentDidMount');
+ log.push(`${this.props.logName} componentDidMount`);
+ }
+ componentWillReceiveProps() {
+ log.push(`${this.props.logName} componentWillReceiveProps`);
+ }
+ componentWillUpdate() {
+ log.push(`${this.props.logName} componentWillUpdate`);
+ }
+ componentDidUpdate() {
+ log.push(`${this.props.logName} componentDidUpdate`);
}
componentWillUnmount() {
- log.push('Inquisitive componentWillUnmount');
+ log.push(`${this.props.logName} componentWillUnmount`);
}
- }
+ };
- class Angry extends React.Component {
+ ErrorBoundary = class extends React.Component {
+ constructor() {
+ super();
+ this.state = {error: null};
+ log.push('ErrorBoundary constructor');
+ }
render() {
- log.push('Angry render');
- throw new Error('Please, do not render me.');
+ if (this.state.error && !this.props.forceRetry) {
+ log.push('ErrorBoundary render error');
+ return this.props.renderError(this.state.error, this.props);
+ }
+ log.push('ErrorBoundary render success');
+ return {this.props.children}
;
+ }
+ unstable_handleError(error) {
+ log.push('ErrorBoundary unstable_handleError');
+ this.setState({error});
+ }
+ componentWillMount() {
+ log.push('ErrorBoundary componentWillMount');
+ }
+ componentDidMount() {
+ log.push('ErrorBoundary componentDidMount');
+ }
+ componentWillReceiveProps() {
+ log.push('ErrorBoundary componentWillReceiveProps');
+ }
+ componentWillUpdate() {
+ log.push('ErrorBoundary componentWillUpdate');
+ }
+ componentDidUpdate() {
+ log.push('ErrorBoundary componentDidUpdate');
+ }
+ componentWillUnmount() {
+ log.push('ErrorBoundary componentWillUnmount');
+ }
+ };
+ ErrorBoundary.defaultProps = {
+ renderError(error, props) {
+ return (
+
+ Caught an error: {error.message}.
+
+ );
+ },
+ };
+
+ ErrorMessage = class extends React.Component {
+ constructor(props) {
+ super(props);
+ log.push('ErrorMessage constructor');
+ }
+ componentWillMount() {
+ log.push('ErrorMessage componentWillMount');
}
componentDidMount() {
- log.push('Angry componentDidMount');
+ log.push('ErrorMessage componentDidMount');
}
componentWillUnmount() {
- log.push('Angry componentWillUnmount');
+ log.push('ErrorMessage componentWillUnmount');
+ }
+ render() {
+ log.push('ErrorMessage render');
+ return Caught an error: {this.props.message}.
;
+ }
+ };
+ });
+
+ // Known limitation: error boundary only "sees" errors caused by updates
+ // flowing through it. This might be easier to fix in Fiber.
+ it('currently does not catch errors originating downstream', () => {
+ var fail = false;
+ class Stateful extends React.Component {
+ state = {shouldThrow: false};
+
+ render() {
+ if (fail) {
+ log.push('Stateful render [!]');
+ throw new Error('Hello');
+ }
+ return ;
}
}
+ var statefulInst;
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+ statefulInst = inst} />
+ ,
+ container
+ );
+
+ log.length = 0;
+ expect(() => {
+ fail = true;
+ statefulInst.forceUpdate();
+ }).toThrow();
+
+ expect(log).toEqual([
+ 'Stateful render [!]',
+ // FIXME: uncomment when downstream errors get caught.
+ // Catch and render an error message
+ // 'ErrorBoundary unstable_handleError',
+ // 'ErrorBoundary render error',
+ // 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('renders an error state if child throws in render', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe(
+ 'Caught an error: Hello.'
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Catch and render an error message
+ 'ErrorBoundary unstable_handleError',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('renders an error state if child throws in constructor', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenConstructor constructor [!]',
+ // Catch and render an error message
+ 'ErrorBoundary unstable_handleError',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('renders an error state if child throws in componentWillMount', () => {
var container = document.createElement('div');
- ReactDOM.render(, container);
- expect(container.textContent).toBe('Error: Please, do not render me.');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillMount constructor',
+ 'BrokenComponentWillMount componentWillMount [!]',
+ // Catch and render an error message
+ 'ErrorBoundary unstable_handleError',
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('mounts the error message if mounting fails', () => {
+ function renderError(error) {
+ return (
+
+ );
+ }
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle the error:
+ 'ErrorBoundary unstable_handleError',
+ 'ErrorBoundary render error',
+ // Mount the error message:
+ 'ErrorMessage constructor',
+ 'ErrorMessage componentWillMount',
+ 'ErrorMessage render',
+ 'ErrorMessage componentDidMount',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'ErrorMessage componentWillUnmount',
+ ]);
+ });
+
+ // Known limitation because componentDidMount() does not occur on the stack.
+ // We could either hardcode searching for parent boundary, or wait for Fiber.
+ it('currently does not catch errors in componentDidMount', () => {
+ var container = document.createElement('div');
+ expect(() => {
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ }).toThrow();
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentDidMount constructor',
+ 'BrokenComponentDidMount componentWillMount',
+ 'BrokenComponentDidMount render',
+ 'BrokenComponentDidMount componentDidMount [!]',
+ // FIXME: uncomment when componentDidMount() gets caught.
+ // Catch and render an error message
+ // 'ErrorBoundary unstable_handleError',
+ // 'ErrorBoundary render error',
+ // 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'BrokenComponentDidMount componentWillUnmount',
+ ]);
+ });
+
+ it('propagates errors on retry on mounting', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'NoopErrorBoundary constructor',
+ 'NoopErrorBoundary componentWillMount',
+ 'NoopErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // The first error boundary catches the error
+ // However, it doesn't adjust its state so next render also fails
+ 'NoopErrorBoundary unstable_handleError',
+ 'NoopErrorBoundary render',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // This time, the error propagates to the higher boundary
+ 'ErrorBoundary unstable_handleError',
+ // Render the error
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('propagates errors inside boundary during componentWillMount', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenComponentWillMountErrorBoundary constructor',
+ 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
+ // The error propagates to the higher boundary
+ 'ErrorBoundary unstable_handleError',
+ // Render the error
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('propagates errors inside boundary while rendering error state', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRenderErrorBoundary constructor',
+ 'BrokenRenderErrorBoundary componentWillMount',
+ 'BrokenRenderErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // The first error boundary catches the error
+ // It adjusts state but throws displaying the message
+ 'BrokenRenderErrorBoundary unstable_handleError',
+ 'BrokenRenderErrorBoundary render error [!]',
+ // The error propagates to the higher boundary
+ 'ErrorBoundary unstable_handleError',
+ // Render the error
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('does not register event handlers for unmounted children', () => {
+ var EventPluginHub = require('EventPluginHub');
+ var container = document.createElement('div');
+ EventPluginHub.putListener = jest.fn();
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+ expect(EventPluginHub.putListener).not.toBeCalled();
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('does not call componentWillUnmount when aborting initial mount', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ // Render first child
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Render second child (it throws)
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Error boundary catches the error
+ 'ErrorBoundary unstable_handleError',
+ // Render the error message
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('resets refs if mounting aborts', () => {
+ function childRef(x) {
+ log.push('Child ref is set to ' + x);
+ }
+ function errorMessageRef(x) {
+ log.push('Error message ref is set to ' + x);
+ }
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ // Handle error:
+ 'ErrorBoundary unstable_handleError',
+ // Child ref wasn't (and won't be) set but there's no harm in clearing:
+ 'Child ref is set to null',
+ 'ErrorBoundary render error',
+ // Ref to error message should get set:
+ 'Error message ref is set to [object HTMLDivElement]',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Error message ref is set to null',
+ ]);
+ });
+
+ it('successfully mounts if no error occurs', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+ Mounted successfully.
+ ,
+ container
+ );
+ expect(container.firstChild.textContent).toBe('Mounted successfully.');
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('catches if child throws in constructor during update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenConstructor will abort rendering:
+ 'BrokenConstructor constructor [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Unmount the previously mounted components:
+ 'Normal componentWillUnmount',
+ // Normal2 does not get lifefycle because it was never mounted
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('catches if child throws in componentWillMount during update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenComponentWillMount will abort rendering:
+ 'BrokenComponentWillMount constructor',
+ 'BrokenComponentWillMount componentWillMount [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Unmount the previously mounted components:
+ 'Normal componentWillUnmount',
+ // Normal2 does not get lifefycle because it was never mounted
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('catches if child throws in componentWillReceiveProps during update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // BrokenComponentWillReceiveProps will abort rendering:
+ 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Unmount the previously mounted components:
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillReceiveProps componentWillUnmount',
+ // Render error:
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('catches if child throws in componentWillUpdate during update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // BrokenComponentWillUpdate will abort rendering:
+ 'BrokenComponentWillUpdate componentWillReceiveProps',
+ 'BrokenComponentWillUpdate componentWillUpdate [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Unmount the previously mounted components:
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUpdate componentWillUnmount',
+ // Render error:
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('catches if child throws in render during update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ // Normal2 will attempt to mount:
+ 'Normal2 constructor',
+ 'Normal2 componentWillMount',
+ 'Normal2 render',
+ // BrokenRender will abort rendering:
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Unmount the previously mounted components:
+ 'Normal componentWillUnmount',
+ // Normal2 does not get lifefycle because it was never mounted
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('keeps refs up-to-date during updates', () => {
+ function child1Ref(x) {
+ log.push('Child1 ref is set to ' + x);
+ }
+ function child2Ref(x) {
+ log.push('Child2 ref is set to ' + x);
+ }
+ function errorMessageRef(x) {
+ log.push('Error message ref is set to ' + x);
+ }
+
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(log).toEqual([
+ 'ErrorBoundary constructor',
+ 'ErrorBoundary componentWillMount',
+ 'ErrorBoundary render success',
+ 'Child1 ref is set to [object HTMLDivElement]',
+ 'ErrorBoundary componentDidMount',
+ ]);
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // BrokenRender will abort rendering:
+ 'BrokenRender constructor',
+ 'BrokenRender componentWillMount',
+ 'BrokenRender render [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Unmount the previously mounted components:
+ 'Child1 ref is set to null',
+ 'ErrorBoundary render error',
+ 'Error message ref is set to [object HTMLDivElement]',
+ // Child2 ref is never set because its mounting aborted
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Error message ref is set to null',
+ ]);
+ });
+
+ // Known limitation because componentDidUpdate() does not occur on the stack.
+ // We could either hardcode searching for parent boundary, or wait for Fiber.
+ it('currently does not catch errors in componentDidUpdate', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ expect(() => {
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ }).toThrow();
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ 'BrokenComponentDidUpdate componentWillReceiveProps',
+ 'BrokenComponentDidUpdate componentWillUpdate',
+ 'BrokenComponentDidUpdate render',
+ 'BrokenComponentDidUpdate componentDidUpdate [!]',
+ // FIXME: uncomment when componentDidUpdate() gets caught.
+ // Catch and render an error message
+ // 'ErrorBoundary unstable_handleError',
+ // 'ErrorBoundary render error',
+ // 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'BrokenComponentDidUpdate componentWillUnmount',
+ ]);
+ });
+
+ it('recovers from componentWillUnmount errors on update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Update existing children:
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ // Unmounting throws:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Attempt to unmount previous children:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Render error:
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ // Children don't get componentDidUpdate() since update was aborted
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('recovers from nested componentWillUnmount errors on update', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+
+ ,
+ container
+ );
+
+ log.length = 0;
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Update existing children:
+ 'Normal componentWillReceiveProps',
+ 'Normal componentWillUpdate',
+ 'Normal render',
+ 'BrokenComponentWillUnmount componentWillReceiveProps',
+ 'BrokenComponentWillUnmount componentWillUpdate',
+ 'BrokenComponentWillUnmount render',
+ // Unmounting throws:
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ 'ErrorBoundary unstable_handleError',
+ // Attempt to unmount previous children:
+ 'Normal componentWillUnmount',
+ 'BrokenComponentWillUnmount componentWillUnmount [!]',
+ // Render error:
+ 'ErrorBoundary render error',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('can recover from error state', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ // Error boundary doesn't retry by itself:
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ // Force the success path:
+ log.length = 0;
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.textContent).not.toContain('Caught an error');
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillReceiveProps',
+ 'ErrorBoundary componentWillUpdate',
+ 'ErrorBoundary render success',
+ // Mount children:
+ 'Normal constructor',
+ 'Normal componentWillMount',
+ 'Normal render',
+ // Finalize updates:
+ 'Normal componentDidMount',
+ 'ErrorBoundary componentDidUpdate',
+ ]);
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ 'Normal componentWillUnmount',
+ ]);
+ });
+
+ it('can update multiple times in error state', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ ReactDOM.render(Other screen
, container);
+ expect(container.textContent).toBe('Other screen');
+
+ ReactDOM.unmountComponentAtNode(container);
+ });
+
+ it('doesn\'t get into inconsistent state during removals', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+
+ ReactDOM.render(, container);
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('doesn\'t get into inconsistent state during additions', () => {
+ var container = document.createElement('div');
+ ReactDOM.render(, container);
+ ReactDOM.render(
+
+
+
+
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(log).toEqual([
+ 'ErrorBoundary componentWillUnmount',
+ ]);
+ });
+
+ it('doesn\'t get into inconsistent state during reorders', () => {
+ function getAMixOfNormalAndBrokenRenderElements() {
+ var elements = [];
+ for (var i = 0; i < 100; i++) {
+ elements.push();
+ }
+ elements.push();
+
+ var currentIndex = elements.length;
+ while (0 !== currentIndex) {
+ var randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+ var temporaryValue = elements[currentIndex];
+ elements[currentIndex] = elements[randomIndex];
+ elements[randomIndex] = temporaryValue;
+ }
+ return elements;
+ }
+
+ class MaybeBrokenRender extends React.Component {
+ render() {
+ if (fail) {
+ throw new Error('Hello');
+ }
+ return ;
+ }
+ }
+
+ var fail = false;
+ var container = document.createElement('div');
+ ReactDOM.render(
+
+ {getAMixOfNormalAndBrokenRenderElements()}
+ ,
+ container
+ );
+ expect(container.textContent).not.toContain('Caught an error');
+
+ fail = true;
+ ReactDOM.render(
+
+ {getAMixOfNormalAndBrokenRenderElements()}
+ ,
+ container
+ );
+ expect(container.textContent).toBe('Caught an error: Hello.');
+
+ log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
- 'Box render',
- 'Inquisitive render',
- 'Angry render',
- 'Inquisitive ref null',
- 'Inquisitive componentWillUnmount',
- 'Box renderError',
- 'Box componentDidMount',
- 'Box componentWillUnmount',
+ 'ErrorBoundary componentWillUnmount',
]);
});
});
diff --git a/src/renderers/testing/ReactTestMount.js b/src/renderers/testing/ReactTestMount.js
index f13e5d20f6d7f..bed525c4315ed 100644
--- a/src/renderers/testing/ReactTestMount.js
+++ b/src/renderers/testing/ReactTestMount.js
@@ -128,7 +128,8 @@ ReactTestInstance.prototype.unmount = function(nextElement) {
transaction.perform(function() {
ReactReconciler.unmountComponent(
component,
- false
+ false, /* safely */
+ false /* skipLifecycle */
);
});
ReactUpdates.ReactReconcileTransaction.release(transaction);
diff --git a/src/test/ReactShallowRenderer.js b/src/test/ReactShallowRenderer.js
index e16f8476117e5..ca2e245b1f992 100644
--- a/src/test/ReactShallowRenderer.js
+++ b/src/test/ReactShallowRenderer.js
@@ -117,7 +117,11 @@ class ReactShallowRenderer {
}
unmount() {
if (this._instance) {
- ReactReconciler.unmountComponent(this._instance, false);
+ ReactReconciler.unmountComponent(
+ this._instance,
+ false, /* safely */
+ false /* skipLifecycle */
+ );
}
}
_render(element, transaction, context) {