mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[Fiber] Error Boundaries (#8095)
This commit is contained in:
@@ -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;
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user