[Fiber] Error Boundaries (#8095)

This commit is contained in:
Dan Abramov
2016-10-28 10:29:14 +01:00
committed by GitHub
parent 308e0b7786
commit 3c6abbfff7
4 changed files with 627 additions and 193 deletions
@@ -12,6 +12,7 @@
'use strict';
import type { TrappedError } from 'ReactFiberErrorBoundary';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
@@ -23,6 +24,7 @@ var {
HostComponent,
HostText,
} = ReactTypeOfWork;
var { trapError } = require('ReactFiberErrorBoundary');
var { callCallbacks } = require('ReactFiberUpdateQueue');
var {
@@ -155,7 +157,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function commitNestedUnmounts(root : Fiber) {
function commitNestedUnmounts(root : Fiber): Array<TrappedError> | null {
// Since errors are rare, we allocate this array on demand.
let trappedErrors = null;
// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
@@ -163,39 +168,58 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// we do an inner loop while we're still inside the host node.
let node : Fiber = root;
while (true) {
commitUnmount(node);
const error = commitUnmount(node);
if (error) {
trappedErrors = trappedErrors || [];
trappedErrors.push(error);
}
if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
continue;
}
if (node === root) {
return;
return trappedErrors;
}
while (!node.sibling) {
if (!node.return || node.return === root) {
return;
return trappedErrors;
}
node = node.return;
}
node = node.sibling;
}
return trappedErrors;
}
function unmountHostComponents(parent, current) {
function unmountHostComponents(parent, current): Array<TrappedError> | null {
// Since errors are rare, we allocate this array on demand.
let trappedErrors = null;
// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
let node : Fiber = current;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
commitNestedUnmounts(node);
const errors = commitNestedUnmounts(node);
if (errors) {
if (!trappedErrors) {
trappedErrors = errors;
} else {
trappedErrors.push.apply(trappedErrors, errors);
}
}
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (parent) {
removeChild(parent, node.stateNode);
}
} else {
commitUnmount(node);
const error = commitUnmount(node);
if (error) {
trappedErrors = trappedErrors || [];
trappedErrors.push(error);
}
if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
@@ -203,24 +227,24 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
if (node === current) {
return;
return trappedErrors;
}
while (!node.sibling) {
if (!node.return || node.return === current) {
return;
return trappedErrors;
}
node = node.return;
}
node = node.sibling;
}
return trappedErrors;
}
function commitDeletion(current : Fiber) : void {
function commitDeletion(current : Fiber) : Array<TrappedError> | null {
// Recursively delete all host nodes from the parent.
// TODO: Error handling.
const parent = getHostParent(current);
unmountHostComponents(parent, current);
// Detach refs and call componentWillUnmount() on the whole subtree.
const trappedErrors = unmountHostComponents(parent, current);
// Cut off the return pointers to disconnect it from the tree. Ideally, we
// should clear the child pointer of the parent alternate to let this
@@ -233,21 +257,29 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
current.alternate.child = null;
current.alternate.return = null;
}
return trappedErrors;
}
function commitUnmount(current : Fiber) : void {
function commitUnmount(current : Fiber) : TrappedError | null {
switch (current.tag) {
case ClassComponent: {
detachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
instance.componentWillUnmount();
const error = tryCallComponentWillUnmount(instance);
if (error) {
return trapError(current, error);
}
}
return;
return null;
}
case HostComponent: {
detachRef(current);
return;
return null;
}
default: {
return null;
}
}
}
@@ -292,19 +324,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : TrappedError | null {
switch (finishedWork.tag) {
case ClassComponent: {
const instance = finishedWork.stateNode;
let error = null;
if (!current) {
if (typeof instance.componentDidMount === 'function') {
instance.componentDidMount();
error = tryCallComponentDidMount(instance);
}
} else {
if (typeof instance.componentDidUpdate === 'function') {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
instance.componentDidUpdate(prevProps, prevState);
error = tryCallComponentDidUpdate(instance, prevProps, prevState);
}
}
// Clear updates from current fiber. This must go before the callbacks
@@ -320,7 +353,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
callCallbacks(callbackList, instance);
}
attachRef(current, finishedWork, instance);
return;
if (error) {
return trapError(finishedWork, error);
}
return null;
}
case HostContainer: {
const instance = finishedWork.stateNode;
@@ -333,17 +369,44 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
case HostComponent: {
const instance : I = finishedWork.stateNode;
attachRef(current, finishedWork, instance);
return;
return null;
}
case HostText: {
// We have no life-cycles associated with text.
return;
return null;
}
default:
throw new Error('This unit of work tag should not have side-effects.');
}
}
function tryCallComponentDidMount(instance) {
try {
instance.componentDidMount();
return null;
} catch (error) {
return error;
}
}
function tryCallComponentDidUpdate(instance, prevProps, prevState) {
try {
instance.componentDidUpdate(prevProps, prevState);
return null;
} catch (error) {
return error;
}
}
function tryCallComponentWillUnmount(instance) {
try {
instance.componentWillUnmount();
return null;
} catch (error) {
return error;
}
}
return {
commitInsertion,
commitDeletion,
@@ -0,0 +1,53 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFiberErrorBoundary
* @flow
*/
'use strict';
import type { Fiber } from 'ReactFiber';
var {
ClassComponent,
} = require('ReactTypeOfWork');
export type TrappedError = {
boundary: Fiber | null,
error: any,
};
function findClosestErrorBoundary(fiber : Fiber): Fiber | null {
let maybeErrorBoundary = fiber.return;
while (maybeErrorBoundary) {
if (maybeErrorBoundary.tag === ClassComponent) {
const instance = maybeErrorBoundary.stateNode;
if (typeof instance.unstable_handleError === 'function') {
return maybeErrorBoundary;
}
}
maybeErrorBoundary = maybeErrorBoundary.return;
}
return null;
}
function trapError(fiber : Fiber, error : any) : TrappedError {
return {
boundary: findClosestErrorBoundary(fiber),
error,
};
}
function acknowledgeErrorInBoundary(boundary : Fiber, error : any) {
const instance = boundary.stateNode;
instance.unstable_handleError(error);
}
exports.trapError = trapError;
exports.acknowledgeErrorInBoundary = acknowledgeErrorInBoundary;
+134 -14
View File
@@ -23,6 +23,7 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactCurrentOwner = require('ReactCurrentOwner');
var { cloneFiber } = require('ReactFiber');
var { trapError, acknowledgeErrorInBoundary } = require('ReactFiberErrorBoundary');
var {
NoWork,
@@ -109,9 +110,14 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
return null;
}
function commitAllWork(finishedWork : Fiber) {
function commitAllWork(finishedWork : Fiber, ignoreUnmountingErrors : boolean) {
// Commit all the side-effects within a tree.
// TODO: Error handling.
// Commit phase is meant to be atomic and non-interruptible.
// Any errors raised in it should be handled after it is over
// so that we don't end up in an inconsistent state due to user code.
// We'll keep track of all caught errors and handle them later.
let allTrappedErrors = null;
// First, we'll perform all the host insertions, updates, deletions and
// ref unmounts.
@@ -129,7 +135,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
commitInsertion(effectfulFiber);
const current = effectfulFiber.alternate;
commitWork(current, effectfulFiber);
// Clear the effect tag so that we know that this is inserted, before
// Clear the "placement" from effect tag so that we know that this is inserted, before
// any life-cycles like componentDidMount gets called.
effectfulFiber.effectTag = Update;
break;
@@ -140,7 +146,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
break;
}
case Deletion: {
commitDeletion(effectfulFiber);
// Deletion might cause an error in componentWillUnmount().
// We will continue nevertheless and handle those later on.
const trappedErrors = commitDeletion(effectfulFiber);
// There is a special case where we completely ignore errors.
// It happens when we already caught an error earlier, and the update
// is caused by an error boundary trying to render an error message.
// In this case, we want to blow away the tree without catching errors.
if (trappedErrors && !ignoreUnmountingErrors) {
if (!allTrappedErrors) {
allTrappedErrors = trappedErrors;
} else {
allTrappedErrors.push.apply(allTrappedErrors, trappedErrors);
}
}
break;
}
}
@@ -155,7 +174,11 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
if (effectfulFiber.effectTag === Update ||
effectfulFiber.effectTag === PlacementAndUpdate) {
const current = effectfulFiber.alternate;
commitLifeCycles(current, effectfulFiber);
const trappedError = commitLifeCycles(current, effectfulFiber);
if (trappedError) {
allTrappedErrors = allTrappedErrors || [];
allTrappedErrors.push(trappedError);
}
}
const next = effectfulFiber.nextEffect;
// Ensure that we clean these up so that we don't accidentally keep them.
@@ -173,7 +196,17 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
if (finishedWork.effectTag !== NoEffect) {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
commitLifeCycles(current, finishedWork);
const trappedError = commitLifeCycles(current, finishedWork);
if (trappedError) {
allTrappedErrors = allTrappedErrors || [];
allTrappedErrors.push(trappedError);
}
}
// Now that the tree has been committed, we can handle errors.
if (allTrappedErrors) {
// TODO: handle multiple errors with distinct boundaries.
handleError(allTrappedErrors[0]);
}
}
@@ -195,7 +228,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
workInProgress.pendingWorkPriority = newPriority;
}
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
function completeUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
while (true) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
@@ -267,7 +300,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// "next" scheduled work since we've already scanned passed. That
// also ensures that work scheduled during reconciliation gets deferred.
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
commitAllWork(workInProgress);
commitAllWork(workInProgress, ignoreUnmountingErrors);
const nextWork = findNextUnitOfWork();
// if (!nextWork && hasMoreWork) {
// TODO: This can happen when some deep work completes and we don't
@@ -281,7 +314,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
function performUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
@@ -302,7 +335,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress);
}
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
next = completeUnitOfWork(workInProgress, ignoreUnmountingErrors);
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress);
}
@@ -313,13 +346,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
return next;
}
function performDeferredWork(deadline) {
function performDeferredWorkUnsafe(deadline) {
if (!nextUnitOfWork) {
nextUnitOfWork = findNextUnitOfWork();
}
while (nextUnitOfWork) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
if (!nextUnitOfWork) {
// Find more work. We might have time to complete some more.
nextUnitOfWork = findNextUnitOfWork();
@@ -331,6 +364,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function performDeferredWork(deadline) {
try {
performDeferredWorkUnsafe(deadline);
} catch (error) {
const failedUnitOfWork = nextUnitOfWork;
// Reset because it points to the error boundary:
nextUnitOfWork = null;
if (!failedUnitOfWork) {
// We shouldn't end up here because nextUnitOfWork
// should always be set while work is being performed.
throw error;
}
const trappedError = trapError(failedUnitOfWork, error);
handleError(trappedError);
}
}
function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) {
// We must reset the current unit of work pointer so that we restart the
// search from the root during the next tick, in case there is now higher
@@ -362,12 +412,12 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function performAnimationWork() {
function performAnimationWorkUnsafe() {
// Always start from the root
nextUnitOfWork = findNextUnitOfWork();
while (nextUnitOfWork &&
nextPriorityLevel !== NoWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
if (!nextUnitOfWork) {
// Keep searching for animation work until there's no more left
nextUnitOfWork = findNextUnitOfWork();
@@ -380,6 +430,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function performAnimationWork() {
try {
performAnimationWorkUnsafe();
} catch (error) {
const failedUnitOfWork = nextUnitOfWork;
// Reset because it points to the error boundary:
nextUnitOfWork = null;
if (!failedUnitOfWork) {
// We shouldn't end up here because nextUnitOfWork
// should always be set while work is being performed.
throw error;
}
const trappedError = trapError(failedUnitOfWork, error);
handleError(trappedError);
}
}
function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) {
// Set the priority on the root, without deprioritizing
if (root.current.pendingWorkPriority === NoWork ||
@@ -404,6 +471,59 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}
function handleError(trappedError) {
const boundary = trappedError.boundary;
const error = trappedError.error;
if (!boundary) {
throw error;
}
try {
// Give error boundary a chance to update its state
acknowledgeErrorInBoundary(boundary, error);
// We will process an update caused by an error boundary with synchronous priority.
// This leaves us free to not keep track of whether a boundary has errored.
// If it errors again, we will just catch the error and synchronously propagate it higher.
// First, traverse upwards and set pending synchronous priority on the whole tree.
let fiber = boundary;
while (fiber) {
fiber.pendingWorkPriority = SynchronousPriority;
if (fiber.alternate) {
fiber.alternate.pendingWorkPriority = SynchronousPriority;
}
if (!fiber.return) {
if (fiber.tag === HostContainer) {
// We found the root.
// Now go to the second phase and update it synchronously.
break;
} else {
throw new Error('Invalid root');
}
}
fiber = fiber.return;
}
if (!fiber) {
throw new Error('Could not find an error boundary root.');
}
// Find the work in progress tree.
const root : FiberRoot = (fiber.stateNode : any);
fiber = root.current.alternate;
// Perform all the work synchronously.
while (fiber) {
fiber = performUnitOfWork(fiber, true);
}
} catch (nextError) {
// Propagate error to the next boundary or rethrow.
const nextTrappedError = trapError(boundary, nextError);
handleError(nextTrappedError);
}
}
function scheduleWork(root : FiberRoot) {
if (defaultPriority === SynchronousPriority) {
throw new Error('Not implemented yet');
@@ -11,6 +11,8 @@
'use strict';
var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
var React;
var ReactDOM;
@@ -33,6 +35,9 @@ describe('ReactErrorBoundaries', () => {
var Normal;
beforeEach(() => {
// TODO: Fiber isn't error resilient and one test can bring down them all.
jest.resetModuleRegistry();
ReactDOM = require('ReactDOM');
React = require('React');
@@ -46,7 +51,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenConstructor render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenConstructor componentWillMount');
@@ -75,7 +80,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenComponentWillMount render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenComponentWillMount componentWillMount [!]');
@@ -105,7 +110,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenComponentDidMount render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenComponentDidMount componentWillMount');
@@ -135,7 +140,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenComponentWillReceiveProps render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenComponentWillReceiveProps componentWillMount');
@@ -165,7 +170,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenComponentWillUpdate render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenComponentWillUpdate componentWillMount');
@@ -195,7 +200,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenComponentDidUpdate render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenComponentDidUpdate componentWillMount');
@@ -225,7 +230,7 @@ describe('ReactErrorBoundaries', () => {
}
render() {
log.push('BrokenComponentWillUnmount render');
return <div />;
return <div>{this.props.children}</div>;
}
componentWillMount() {
log.push('BrokenComponentWillUnmount componentWillMount');
@@ -459,52 +464,52 @@ describe('ReactErrorBoundaries', () => {
};
});
// 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};
if (ReactDOMFeatureFlags.useFiber) {
// This test implements a new feature in Fiber.
it('catches 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');
render() {
if (fail) {
log.push('Stateful render [!]');
throw new Error('Hello');
}
return <div>{this.props.children}</div>;
}
return <div />;
}
}
var statefulInst;
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<Stateful ref={inst => statefulInst = inst} />
</ErrorBoundary>,
container
);
var statefulInst;
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<Stateful ref={inst => statefulInst = inst} />
</ErrorBoundary>,
container
);
log.length = 0;
expect(() => {
fail = true;
statefulInst.forceUpdate();
}).toThrow();
log.length = 0;
expect(() => {
fail = true;
statefulInst.forceUpdate();
}).not.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',
]);
expect(log).toEqual([
'Stateful render [!]',
'ErrorBoundary unstable_handleError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
]);
});
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');
@@ -526,6 +531,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender render [!]',
// Catch and render an error message
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
@@ -553,6 +564,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenConstructor constructor [!]',
// Catch and render an error message
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
@@ -581,6 +598,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentWillMount componentWillMount [!]',
// Catch and render an error message
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
]);
@@ -615,6 +638,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender render [!]',
// Handle the error:
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
'ErrorBoundary render error',
// Mount the error message:
'ErrorMessage constructor',
@@ -632,40 +661,64 @@ describe('ReactErrorBoundaries', () => {
]);
});
// 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(() => {
if (ReactDOMFeatureFlags.useFiber) {
// This test implements a new feature in Fiber.
it('catches errors in componentDidMount', () => {
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<BrokenComponentWillUnmount>
<Normal />
</BrokenComponentWillUnmount>
<BrokenComponentDidMount />
<Normal logName="LastChild" />
</ErrorBoundary>,
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',
]);
expect(log).toEqual([
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenComponentWillUnmount constructor',
'BrokenComponentWillUnmount componentWillMount',
'BrokenComponentWillUnmount render',
'Normal constructor',
'Normal componentWillMount',
'Normal render',
'BrokenComponentDidMount constructor',
'BrokenComponentDidMount componentWillMount',
'BrokenComponentDidMount render',
'LastChild constructor',
'LastChild componentWillMount',
'LastChild render',
// Start flushing didMount queue
'Normal componentDidMount',
'BrokenComponentWillUnmount componentDidMount',
'BrokenComponentDidMount componentDidMount [!]',
// Continue despite the error
'LastChild componentDidMount',
'ErrorBoundary componentDidMount',
// Now we are ready to handle the error
'ErrorBoundary unstable_handleError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
// Safely unmount every child
'BrokenComponentWillUnmount componentWillUnmount [!]',
// Continue unmounting safely despite any errors
'Normal componentWillUnmount',
'BrokenComponentDidMount componentWillUnmount',
'LastChild componentWillUnmount',
// The update has finished
'ErrorBoundary componentDidUpdate',
]);
log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
'BrokenComponentDidMount componentWillUnmount',
]);
});
log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
]);
});
}
it('propagates errors on retry on mounting', () => {
var container = document.createElement('div');
@@ -688,15 +741,30 @@ describe('ReactErrorBoundaries', () => {
'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
// The first error boundary catches the error.
// However, it doesn't adjust its state so next render will also fail.
'NoopErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'NoopErrorBoundary constructor',
'NoopErrorBoundary componentWillMount',
] : []),
'NoopErrorBoundary render',
'BrokenRender constructor',
'BrokenRender componentWillMount',
'BrokenRender render [!]',
// This time, the error propagates to the higher boundary
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
// Render the error
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
@@ -726,6 +794,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
// The error propagates to the higher boundary
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
// Render the error
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
@@ -762,9 +836,24 @@ describe('ReactErrorBoundaries', () => {
// The first error boundary catches the error
// It adjusts state but throws displaying the message
'BrokenRenderErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
'ErrorBoundary render success',
'BrokenRenderErrorBoundary constructor',
'BrokenRenderErrorBoundary componentWillMount',
] : []),
'BrokenRenderErrorBoundary render error [!]',
// The error propagates to the higher boundary
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
// Render the error
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
@@ -822,6 +911,12 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender render [!]',
// Error boundary catches the error
'ErrorBoundary unstable_handleError',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : []),
// Render the error message
'ErrorBoundary render error',
'ErrorBoundary componentDidMount',
@@ -860,8 +955,16 @@ describe('ReactErrorBoundaries', () => {
'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',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary constructor',
'ErrorBoundary componentWillMount',
] : [
// Stack reconciler resets ref on update, as it doesn't know ref was never set.
// This is unnecessary, and Fiber doesn't do it:
'Child ref is set to null',
]),
'ErrorBoundary render error',
// Ref to error message should get set:
'Error message ref is set to [object HTMLDivElement]',
@@ -932,10 +1035,20 @@ describe('ReactErrorBoundaries', () => {
// BrokenConstructor will abort rendering:
'BrokenConstructor constructor [!]',
'ErrorBoundary unstable_handleError',
// Unmount the previously mounted components:
'Normal componentWillUnmount',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
// Fiber renders first, then unmounts in a batch:
'ErrorBoundary render error',
'Normal componentWillUnmount',
] : [
// Stack unmounts first, then renders:
'Normal componentWillUnmount',
'ErrorBoundary render error',
]),
// Normal2 does not get lifefycle because it was never mounted
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
@@ -980,10 +1093,20 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentWillMount constructor',
'BrokenComponentWillMount componentWillMount [!]',
'ErrorBoundary unstable_handleError',
// Unmount the previously mounted components:
'Normal componentWillUnmount',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
// Fiber renders first, then unmounts in a batch:
'ErrorBoundary render error',
'Normal componentWillUnmount',
] : [
// Stack unmounts first, then renders:
'Normal componentWillUnmount',
'ErrorBoundary render error',
]),
// Normal2 does not get lifefycle because it was never mounted
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
@@ -1023,11 +1146,21 @@ describe('ReactErrorBoundaries', () => {
// BrokenComponentWillReceiveProps will abort rendering:
'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
'ErrorBoundary unstable_handleError',
// Unmount the previously mounted components:
'Normal componentWillUnmount',
'BrokenComponentWillReceiveProps componentWillUnmount',
// Render error:
'ErrorBoundary render error',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
// Fiber renders first, then unmounts in a batch:
'ErrorBoundary render error',
'Normal componentWillUnmount',
'BrokenComponentWillReceiveProps componentWillUnmount',
] : [
// Stack unmounts first, then renders:
'Normal componentWillUnmount',
'BrokenComponentWillReceiveProps componentWillUnmount',
'ErrorBoundary render error',
]),
'ErrorBoundary componentDidUpdate',
]);
@@ -1068,11 +1201,21 @@ describe('ReactErrorBoundaries', () => {
'BrokenComponentWillUpdate componentWillReceiveProps',
'BrokenComponentWillUpdate componentWillUpdate [!]',
'ErrorBoundary unstable_handleError',
// Unmount the previously mounted components:
'Normal componentWillUnmount',
'BrokenComponentWillUpdate componentWillUnmount',
// Render error:
'ErrorBoundary render error',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
// Fiber renders first, then unmounts in a batch:
'ErrorBoundary render error',
'Normal componentWillUnmount',
'BrokenComponentWillUpdate componentWillUnmount',
] : [
// Stack unmounts first, then renders:
'Normal componentWillUnmount',
'BrokenComponentWillUpdate componentWillUnmount',
'ErrorBoundary render error',
]),
'ErrorBoundary componentDidUpdate',
]);
@@ -1118,10 +1261,20 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary unstable_handleError',
// Unmount the previously mounted components:
'Normal componentWillUnmount',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
// Fiber renders first, then unmounts in a batch:
'ErrorBoundary render error',
'Normal componentWillUnmount',
] : [
// Stack unmounts first, then renders:
'Normal componentWillUnmount',
'ErrorBoundary render error',
]),
// Normal2 does not get lifefycle because it was never mounted
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);
@@ -1177,9 +1330,19 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender componentWillMount',
'BrokenRender render [!]',
'ErrorBoundary unstable_handleError',
// Unmount the previously mounted components:
'Child1 ref is set to null',
'ErrorBoundary render error',
...(ReactDOMFeatureFlags.useFiber ? [
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
// Fiber renders first, resets refs later
'ErrorBoundary render error',
'Child1 ref is set to null',
] : [
// Stack resets ref first, renders later
'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',
@@ -1193,48 +1356,49 @@ describe('ReactErrorBoundaries', () => {
]);
});
// 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(
<ErrorBoundary>
<BrokenComponentDidUpdate />
</ErrorBoundary>,
container
);
log.length = 0;
expect(() => {
if (ReactDOMFeatureFlags.useFiber) {
// This test implements a new feature in Fiber.
it('catches errors in componentDidUpdate', () => {
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<BrokenComponentDidUpdate />
</ErrorBoundary>,
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',
]);
});
log.length = 0;
ReactDOM.render(
<ErrorBoundary>
<BrokenComponentDidUpdate />
</ErrorBoundary>,
container
);
expect(log).toEqual([
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
'BrokenComponentDidUpdate componentWillReceiveProps',
'BrokenComponentDidUpdate componentWillUpdate',
'BrokenComponentDidUpdate render',
// All lifecycles run
'BrokenComponentDidUpdate componentDidUpdate [!]',
'ErrorBoundary componentDidUpdate',
// Then, error is handled
'ErrorBoundary unstable_handleError',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render error',
'BrokenComponentDidUpdate componentWillUnmount',
'ErrorBoundary componentDidUpdate',
]);
log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
]);
});
}
it('recovers from componentWillUnmount errors on update', () => {
var container = document.createElement('div');
@@ -1242,7 +1406,7 @@ describe('ReactErrorBoundaries', () => {
<ErrorBoundary>
<BrokenComponentWillUnmount />
<BrokenComponentWillUnmount />
<BrokenComponentWillUnmount />
<Normal />
</ErrorBoundary>,
container
);
@@ -1251,7 +1415,6 @@ describe('ReactErrorBoundaries', () => {
ReactDOM.render(
<ErrorBoundary>
<BrokenComponentWillUnmount />
<BrokenComponentWillUnmount />
</ErrorBoundary>,
container
);
@@ -1260,23 +1423,39 @@ describe('ReactErrorBoundaries', () => {
'ErrorBoundary componentWillReceiveProps',
'ErrorBoundary componentWillUpdate',
'ErrorBoundary render success',
// Update existing children:
'BrokenComponentWillUnmount componentWillReceiveProps',
'BrokenComponentWillUnmount componentWillUpdate',
'BrokenComponentWillUnmount render',
// Update existing child:
'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
...(ReactDOMFeatureFlags.useFiber ? [
// Fiber proceeds with lifecycles despite errors
'Normal componentWillUnmount',
// The components have updated in this phase
'BrokenComponentWillUnmount componentDidUpdate',
'ErrorBoundary componentDidUpdate',
// Now that commit phase is done, Fiber handles errors
'ErrorBoundary unstable_handleError',
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillUpdate',
// Render an error now (stack will do it later)
'ErrorBoundary render error',
// Attempt to unmount previous child:
'BrokenComponentWillUnmount componentWillUnmount [!]',
// Done
'ErrorBoundary componentDidUpdate',
] : [
// Stack will handle error immediately
'ErrorBoundary unstable_handleError',
// Attempt to unmount previous children:
'BrokenComponentWillUnmount componentWillUnmount [!]',
'Normal componentWillUnmount',
// Render an error now (Fiber will do it earlier)
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]),
]);
log.length = 0;
@@ -1321,13 +1500,32 @@ describe('ReactErrorBoundaries', () => {
'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',
...(ReactDOMFeatureFlags.useFiber ? [
// Fiber proceeds with lifecycles despite errors
'BrokenComponentWillUnmount componentDidUpdate',
'Normal componentDidUpdate',
'ErrorBoundary componentDidUpdate',
// Now that commit phase is done, Fiber handles errors
'ErrorBoundary unstable_handleError',
// The initial render was aborted, so
// Fiber retries from the root.
'ErrorBoundary componentWillUpdate',
// Render an error now (stack will do it later)
'ErrorBoundary render error',
// Attempt to unmount previous child:
'Normal componentWillUnmount',
'BrokenComponentWillUnmount componentWillUnmount [!]',
// Done
'ErrorBoundary componentDidUpdate',
] : [
'ErrorBoundary unstable_handleError',
// Attempt to unmount previous children:
'Normal componentWillUnmount',
'BrokenComponentWillUnmount componentWillUnmount [!]',
// Stack calls lifecycles first, then renders.
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]),
]);
log.length = 0;
@@ -1474,7 +1672,7 @@ describe('ReactErrorBoundaries', () => {
if (fail) {
throw new Error('Hello');
}
return <div />;
return <div>{this.props.children}</div>;
}
}