Skip to content

Commit c74790f

Browse files
author
Brian Vaughn
committed
Always skip unmounted/unmounting error boundaries
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).
1 parent ffb749c commit c74790f

7 files changed

+351
-62
lines changed

packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,4 +2473,88 @@ describe('ReactErrorBoundaries', () => {
24732473
'Caught an error: gotta catch em all.',
24742474
);
24752475
});
2476+
2477+
it('catches errors thrown in componentWillUnmount', () => {
2478+
class LocalErrorBoundary extends React.Component {
2479+
state = {error: null};
2480+
static getDerivedStateFromError(error) {
2481+
Scheduler.unstable_yieldValue(
2482+
`ErrorBoundary static getDerivedStateFromError`,
2483+
);
2484+
return {error};
2485+
}
2486+
render() {
2487+
const {children, id, fallbackID} = this.props;
2488+
const {error} = this.state;
2489+
if (error) {
2490+
Scheduler.unstable_yieldValue(`${id} render error`);
2491+
return <Component id={fallbackID} />;
2492+
}
2493+
Scheduler.unstable_yieldValue(`${id} render success`);
2494+
return children || null;
2495+
}
2496+
}
2497+
2498+
class Component extends React.Component {
2499+
render() {
2500+
const {id} = this.props;
2501+
Scheduler.unstable_yieldValue('Component render ' + id);
2502+
return id;
2503+
}
2504+
}
2505+
2506+
class LocalBrokenComponentWillUnmount extends React.Component {
2507+
componentWillUnmount() {
2508+
Scheduler.unstable_yieldValue(
2509+
'BrokenComponentWillUnmount componentWillUnmount',
2510+
);
2511+
throw Error('Expected');
2512+
}
2513+
2514+
render() {
2515+
Scheduler.unstable_yieldValue('BrokenComponentWillUnmount render');
2516+
return 'broken';
2517+
}
2518+
}
2519+
2520+
const container = document.createElement('div');
2521+
2522+
ReactDOM.render(
2523+
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2524+
<Component id="sibling" />
2525+
<LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2526+
<LocalBrokenComponentWillUnmount />
2527+
</LocalErrorBoundary>
2528+
</LocalErrorBoundary>,
2529+
container,
2530+
);
2531+
2532+
expect(container.firstChild.textContent).toBe('sibling');
2533+
expect(container.lastChild.textContent).toBe('broken');
2534+
expect(Scheduler).toHaveYielded([
2535+
'OuterBoundary render success',
2536+
'Component render sibling',
2537+
'InnerBoundary render success',
2538+
'BrokenComponentWillUnmount render',
2539+
]);
2540+
2541+
ReactDOM.render(
2542+
<LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2543+
<Component id="sibling" />
2544+
</LocalErrorBoundary>,
2545+
container,
2546+
);
2547+
2548+
// React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2549+
expect(container.firstChild.textContent).toBe('OuterFallback');
2550+
expect(container.lastChild.textContent).toBe('OuterFallback');
2551+
expect(Scheduler).toHaveYielded([
2552+
'OuterBoundary render success',
2553+
'Component render sibling',
2554+
'BrokenComponentWillUnmount componentWillUnmount',
2555+
'ErrorBoundary static getDerivedStateFromError',
2556+
'OuterBoundary render error',
2557+
'Component render OuterFallback',
2558+
]);
2559+
});
24762560
});

packages/react-reconciler/src/ReactFiberCommitWork.new.js

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,11 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
162162
};
163163

164164
// Capture errors so they don't interrupt unmounting.
165-
function safelyCallComponentWillUnmount(current, instance) {
165+
function safelyCallComponentWillUnmount(
166+
current: Fiber,
167+
instance: any,
168+
nearestMountedAncestor: Fiber,
169+
) {
166170
if (__DEV__) {
167171
invokeGuardedCallback(
168172
null,
@@ -173,13 +177,13 @@ function safelyCallComponentWillUnmount(current, instance) {
173177
);
174178
if (hasCaughtError()) {
175179
const unmountError = clearCaughtError();
176-
captureCommitPhaseError(current, current.return, unmountError);
180+
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
177181
}
178182
} else {
179183
try {
180184
callComponentWillUnmountWithTimer(current, instance);
181185
} catch (unmountError) {
182-
captureCommitPhaseError(current, current.return, unmountError);
186+
captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
183187
}
184188
}
185189
}
@@ -974,6 +978,7 @@ function commitDetachRef(current: Fiber) {
974978
function commitUnmount(
975979
finishedRoot: FiberRoot,
976980
current: Fiber,
981+
nearestMountedAncestor: Fiber,
977982
renderPriorityLevel: ReactPriorityLevel,
978983
): void {
979984
onCommitUnmount(current);
@@ -1001,10 +1006,10 @@ function commitUnmount(
10011006
current.mode & ProfileMode
10021007
) {
10031008
startLayoutEffectTimer();
1004-
safelyCallDestroy(current, current.return, destroy);
1009+
safelyCallDestroy(current, nearestMountedAncestor, destroy);
10051010
recordLayoutEffectDuration(current);
10061011
} else {
1007-
safelyCallDestroy(current, current.return, destroy);
1012+
safelyCallDestroy(current, nearestMountedAncestor, destroy);
10081013
}
10091014
}
10101015
}
@@ -1018,7 +1023,11 @@ function commitUnmount(
10181023
safelyDetachRef(current);
10191024
const instance = current.stateNode;
10201025
if (typeof instance.componentWillUnmount === 'function') {
1021-
safelyCallComponentWillUnmount(current, instance);
1026+
safelyCallComponentWillUnmount(
1027+
current,
1028+
instance,
1029+
nearestMountedAncestor,
1030+
);
10221031
}
10231032
return;
10241033
}
@@ -1031,7 +1040,12 @@ function commitUnmount(
10311040
// We are also not using this parent because
10321041
// the portal will get pushed immediately.
10331042
if (supportsMutation) {
1034-
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1043+
unmountHostComponents(
1044+
finishedRoot,
1045+
current,
1046+
nearestMountedAncestor,
1047+
renderPriorityLevel,
1048+
);
10351049
} else if (supportsPersistence) {
10361050
emptyPortalContainer(current);
10371051
}
@@ -1071,6 +1085,7 @@ function commitUnmount(
10711085
function commitNestedUnmounts(
10721086
finishedRoot: FiberRoot,
10731087
root: Fiber,
1088+
nearestMountedAncestor: Fiber,
10741089
renderPriorityLevel: ReactPriorityLevel,
10751090
): void {
10761091
// While we're inside a removed host node we don't want to call
@@ -1080,7 +1095,12 @@ function commitNestedUnmounts(
10801095
// we do an inner loop while we're still inside the host node.
10811096
let node: Fiber = root;
10821097
while (true) {
1083-
commitUnmount(finishedRoot, node, renderPriorityLevel);
1098+
commitUnmount(
1099+
finishedRoot,
1100+
node,
1101+
nearestMountedAncestor,
1102+
renderPriorityLevel,
1103+
);
10841104
// Visit children because they may contain more composite or host nodes.
10851105
// Skip portals because commitUnmount() currently visits them recursively.
10861106
if (
@@ -1361,9 +1381,10 @@ function insertOrAppendPlacementNode(
13611381
}
13621382

13631383
function unmountHostComponents(
1364-
finishedRoot,
1365-
current,
1366-
renderPriorityLevel,
1384+
finishedRoot: FiberRoot,
1385+
current: Fiber,
1386+
nearestMountedAncestor: Fiber,
1387+
renderPriorityLevel: ReactPriorityLevel,
13671388
): void {
13681389
// We only have the top Fiber that was deleted but we need to recurse down its
13691390
// children to find all the terminal nodes.
@@ -1412,7 +1433,12 @@ function unmountHostComponents(
14121433
}
14131434

14141435
if (node.tag === HostComponent || node.tag === HostText) {
1415-
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1436+
commitNestedUnmounts(
1437+
finishedRoot,
1438+
node,
1439+
nearestMountedAncestor,
1440+
renderPriorityLevel,
1441+
);
14161442
// After all the children have unmounted, it is now safe to remove the
14171443
// node from the tree.
14181444
if (currentParentIsContainer) {
@@ -1429,7 +1455,12 @@ function unmountHostComponents(
14291455
// Don't visit children because we already visited them.
14301456
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
14311457
const fundamentalNode = node.stateNode.instance;
1432-
commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1458+
commitNestedUnmounts(
1459+
finishedRoot,
1460+
node,
1461+
nearestMountedAncestor,
1462+
renderPriorityLevel,
1463+
);
14331464
// After all the children have unmounted, it is now safe to remove the
14341465
// node from the tree.
14351466
if (currentParentIsContainer) {
@@ -1481,7 +1512,12 @@ function unmountHostComponents(
14811512
continue;
14821513
}
14831514
} else {
1484-
commitUnmount(finishedRoot, node, renderPriorityLevel);
1515+
commitUnmount(
1516+
finishedRoot,
1517+
node,
1518+
nearestMountedAncestor,
1519+
renderPriorityLevel,
1520+
);
14851521
// Visit children because we may find more host components below.
14861522
if (node.child !== null) {
14871523
node.child.return = node;
@@ -1511,15 +1547,26 @@ function unmountHostComponents(
15111547
function commitDeletion(
15121548
finishedRoot: FiberRoot,
15131549
current: Fiber,
1550+
nearestMountedAncestor: Fiber,
15141551
renderPriorityLevel: ReactPriorityLevel,
15151552
): void {
15161553
if (supportsMutation) {
15171554
// Recursively delete all host nodes from the parent.
15181555
// Detach refs and call componentWillUnmount() on the whole subtree.
1519-
unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1556+
unmountHostComponents(
1557+
finishedRoot,
1558+
current,
1559+
nearestMountedAncestor,
1560+
renderPriorityLevel,
1561+
);
15201562
} else {
15211563
// Detach refs and call componentWillUnmount() on the whole subtree.
1522-
commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
1564+
commitNestedUnmounts(
1565+
finishedRoot,
1566+
current,
1567+
nearestMountedAncestor,
1568+
renderPriorityLevel,
1569+
);
15231570
}
15241571
const alternate = current.alternate;
15251572
detachFiberMutation(current);

0 commit comments

Comments
 (0)