Skip to content

Commit

Permalink
[Fiber] Error Boundaries (facebook#8095)
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon authored and acusti committed Mar 15, 2017
1 parent d55bcca commit 40dd651
Show file tree
Hide file tree
Showing 4 changed files with 631 additions and 197 deletions.
109 changes: 86 additions & 23 deletions src/renderers/shared/fiber/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,6 +24,7 @@ var {
HostComponent,
HostText,
} = ReactTypeOfWork;
var { trapError } = require('ReactFiberErrorBoundary');
var { callCallbacks } = require('ReactFiberUpdateQueue');

var {
Expand Down Expand Up @@ -155,72 +157,94 @@ 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
// composites before this host node is removed from the tree. Therefore
// 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;
continue;
}
}
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
Expand All @@ -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;
}
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions src/renderers/shared/fiber/ReactFiberErrorBoundary.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 40dd651

Please sign in to comment.