Skip to content

Commit

Permalink
Always skip unmounted/unmounting error boundaries
Browse files Browse the repository at this point in the history
The behavior of error boundaries for passive effects that throw during cleanup was recently changed so that React ignores boundaries which are also unmounting in favor of still-mounted boundaries. This commit implements that same behavior for layout effects (useLayoutEffect and componentWillUnmount).
  • Loading branch information
Brian Vaughn committed Aug 17, 2020
1 parent ffb749c commit c74790f
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2473,4 +2473,88 @@ describe('ReactErrorBoundaries', () => {
'Caught an error: gotta catch em all.',
);
});

it('catches errors thrown in componentWillUnmount', () => {
class LocalErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
Scheduler.unstable_yieldValue(
`ErrorBoundary static getDerivedStateFromError`,
);
return {error};
}
render() {
const {children, id, fallbackID} = this.props;
const {error} = this.state;
if (error) {
Scheduler.unstable_yieldValue(`${id} render error`);
return <Component id={fallbackID} />;
}
Scheduler.unstable_yieldValue(`${id} render success`);
return children || null;
}
}

class Component extends React.Component {
render() {
const {id} = this.props;
Scheduler.unstable_yieldValue('Component render ' + id);
return id;
}
}

class LocalBrokenComponentWillUnmount extends React.Component {
componentWillUnmount() {
Scheduler.unstable_yieldValue(
'BrokenComponentWillUnmount componentWillUnmount',
);
throw Error('Expected');
}

render() {
Scheduler.unstable_yieldValue('BrokenComponentWillUnmount render');
return 'broken';
}
}

const container = document.createElement('div');

ReactDOM.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
<LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
<LocalBrokenComponentWillUnmount />
</LocalErrorBoundary>
</LocalErrorBoundary>,
container,
);

expect(container.firstChild.textContent).toBe('sibling');
expect(container.lastChild.textContent).toBe('broken');
expect(Scheduler).toHaveYielded([
'OuterBoundary render success',
'Component render sibling',
'InnerBoundary render success',
'BrokenComponentWillUnmount render',
]);

ReactDOM.render(
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
<Component id="sibling" />
</LocalErrorBoundary>,
container,
);

// React should skip over the unmounting boundary and find the nearest still-mounted boundary.
expect(container.firstChild.textContent).toBe('OuterFallback');
expect(container.lastChild.textContent).toBe('OuterFallback');
expect(Scheduler).toHaveYielded([
'OuterBoundary render success',
'Component render sibling',
'BrokenComponentWillUnmount componentWillUnmount',
'ErrorBoundary static getDerivedStateFromError',
'OuterBoundary render error',
'Component render OuterFallback',
]);
});
});
79 changes: 63 additions & 16 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
};

// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(current, instance) {
function safelyCallComponentWillUnmount(
current: Fiber,
instance: any,
nearestMountedAncestor: Fiber,
) {
if (__DEV__) {
invokeGuardedCallback(
null,
Expand All @@ -173,13 +177,13 @@ function safelyCallComponentWillUnmount(current, instance) {
);
if (hasCaughtError()) {
const unmountError = clearCaughtError();
captureCommitPhaseError(current, current.return, unmountError);
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
}
} else {
try {
callComponentWillUnmountWithTimer(current, instance);
} catch (unmountError) {
captureCommitPhaseError(current, current.return, unmountError);
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
}
}
}
Expand Down Expand Up @@ -974,6 +978,7 @@ function commitDetachRef(current: Fiber) {
function commitUnmount(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
onCommitUnmount(current);
Expand Down Expand Up @@ -1001,10 +1006,10 @@ function commitUnmount(
current.mode & ProfileMode
) {
startLayoutEffectTimer();
safelyCallDestroy(current, current.return, destroy);
safelyCallDestroy(current, nearestMountedAncestor, destroy);
recordLayoutEffectDuration(current);
} else {
safelyCallDestroy(current, current.return, destroy);
safelyCallDestroy(current, nearestMountedAncestor, destroy);
}
}
}
Expand All @@ -1018,7 +1023,11 @@ function commitUnmount(
safelyDetachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
safelyCallComponentWillUnmount(
current,
instance,
nearestMountedAncestor,
);
}
return;
}
Expand All @@ -1031,7 +1040,12 @@ function commitUnmount(
// We are also not using this parent because
// the portal will get pushed immediately.
if (supportsMutation) {
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
unmountHostComponents(
finishedRoot,
current,
nearestMountedAncestor,
renderPriorityLevel,
);
} else if (supportsPersistence) {
emptyPortalContainer(current);
}
Expand Down Expand Up @@ -1071,6 +1085,7 @@ function commitUnmount(
function commitNestedUnmounts(
finishedRoot: FiberRoot,
root: Fiber,
nearestMountedAncestor: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
// While we're inside a removed host node we don't want to call
Expand All @@ -1080,7 +1095,12 @@ function commitNestedUnmounts(
// we do an inner loop while we're still inside the host node.
let node: Fiber = root;
while (true) {
commitUnmount(finishedRoot, node, renderPriorityLevel);
commitUnmount(
finishedRoot,
node,
nearestMountedAncestor,
renderPriorityLevel,
);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
if (
Expand Down Expand Up @@ -1361,9 +1381,10 @@ function insertOrAppendPlacementNode(
}

function unmountHostComponents(
finishedRoot,
current,
renderPriorityLevel,
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
Expand Down Expand Up @@ -1412,7 +1433,12 @@ function unmountHostComponents(
}

if (node.tag === HostComponent || node.tag === HostText) {
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
commitNestedUnmounts(
finishedRoot,
node,
nearestMountedAncestor,
renderPriorityLevel,
);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
Expand All @@ -1429,7 +1455,12 @@ function unmountHostComponents(
// Don't visit children because we already visited them.
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
const fundamentalNode = node.stateNode.instance;
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
commitNestedUnmounts(
finishedRoot,
node,
nearestMountedAncestor,
renderPriorityLevel,
);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
Expand Down Expand Up @@ -1481,7 +1512,12 @@ function unmountHostComponents(
continue;
}
} else {
commitUnmount(finishedRoot, node, renderPriorityLevel);
commitUnmount(
finishedRoot,
node,
nearestMountedAncestor,
renderPriorityLevel,
);
// Visit children because we may find more host components below.
if (node.child !== null) {
node.child.return = node;
Expand Down Expand Up @@ -1511,15 +1547,26 @@ function unmountHostComponents(
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
unmountHostComponents(
finishedRoot,
current,
nearestMountedAncestor,
renderPriorityLevel,
);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
commitNestedUnmounts(
finishedRoot,
current,
nearestMountedAncestor,
renderPriorityLevel,
);
}
const alternate = current.alternate;
detachFiberMutation(current);
Expand Down
Loading

0 comments on commit c74790f

Please sign in to comment.