Improve error boundary handling for unmounted subtrees (#20645)
A passive effect's cleanup function may throw after an unmount. Prior to this commit, such an error would be ignored. (React would not notify any error boundaries.) After this commit, React will skip any unmounted boundaries and look for a still-mounted boundary. If one is found, it will call getDerivedStateFromError and/or componentDidCatch (depending on the type of boundary). Unmounted boundaries will be ignored, but as they have been unmounted– this seems appropriate.
Brian Vaughn committed
Jan 25, 2021 at 05:54 UTC
895ae67fd3cb16b23d66a8be2ad1c747188a811f
17 files changed
+794
-114
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+170
@@ -42,6 +42,7 @@ describe('ReactErrorBoundaries', () => {
42
PropTypes = require('prop-types');
43
ReactFeatureFlags = require('shared/ReactFeatureFlags');
44
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
45
+ ReactFeatureFlags.skipUnmountedBoundaries = true;
46
ReactDOM = require('react-dom');
47
React = require('react');
48
act = require('react-dom/test-utils').unstable_concurrentAct;
@@ -2473,4 +2474,173 @@ describe('ReactErrorBoundaries', () => {
2474
'Caught an error: gotta catch em all.',
2475
);
2476
});
2477
+
2478
+ it('catches errors thrown in componentWillUnmount', () => {
2479
+ class LocalErrorBoundary extends React.Component {
2480
+ state = {error: null};
2481
+ static getDerivedStateFromError(error) {
2482
+ Scheduler.unstable_yieldValue(
2483
+ `ErrorBoundary static getDerivedStateFromError`,
2484
+ );
2485
+ return {error};
2486
+ }
2487
+ render() {
2488
+ const {children, id, fallbackID} = this.props;
2489
+ const {error} = this.state;
2490
+ if (error) {
2491
+ Scheduler.unstable_yieldValue(`${id} render error`);
2492
+ return <Component id={fallbackID} />;
2493
+ }
2494
+ Scheduler.unstable_yieldValue(`${id} render success`);
2495
+ return children || null;
2496
+ }
2497
+ }
2498
+
2499
+ class Component extends React.Component {
2500
+ render() {
2501
+ const {id} = this.props;
2502
+ Scheduler.unstable_yieldValue('Component render ' + id);
2503
+ return id;
2504
+ }
2505
+ }
2506
+
2507
+ class LocalBrokenComponentWillUnmount extends React.Component {
2508
+ componentWillUnmount() {
2509
+ Scheduler.unstable_yieldValue(
2510
+ 'BrokenComponentWillUnmount componentWillUnmount',
2511
+ );
2512
+ throw Error('Expected');
2513
+ }
2514
+
2515
+ render() {
2516
+ Scheduler.unstable_yieldValue('BrokenComponentWillUnmount render');
2517
+ return 'broken';
2518
+ }
2519
+ }
2520
+
2521
+ const container = document.createElement('div');
2522
+
2523
+ ReactDOM.render(
2524
+ <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2525
+ <Component id="sibling" />
2526
+ <LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2527
+ <LocalBrokenComponentWillUnmount />
2528
+ </LocalErrorBoundary>
2529
+ </LocalErrorBoundary>,
2530
+ container,
2531
+ );
2532
+
2533
+ expect(container.firstChild.textContent).toBe('sibling');
2534
+ expect(container.lastChild.textContent).toBe('broken');
2535
+ expect(Scheduler).toHaveYielded([
2536
+ 'OuterBoundary render success',
2537
+ 'Component render sibling',
2538
+ 'InnerBoundary render success',
2539
+ 'BrokenComponentWillUnmount render',
2540
+ ]);
2541
+
2542
+ ReactDOM.render(
2543
+ <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2544
+ <Component id="sibling" />
2545
+ </LocalErrorBoundary>,
2546
+ container,
2547
+ );
2548
+
2549
+ // React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2550
+ expect(container.firstChild.textContent).toBe('OuterFallback');
2551
+ expect(container.lastChild.textContent).toBe('OuterFallback');
2552
+ expect(Scheduler).toHaveYielded([
2553
+ 'OuterBoundary render success',
2554
+ 'Component render sibling',
2555
+ 'BrokenComponentWillUnmount componentWillUnmount',
2556
+ 'ErrorBoundary static getDerivedStateFromError',
2557
+ 'OuterBoundary render error',
2558
+ 'Component render OuterFallback',
2559
+ ]);
2560
+ });
2561
+
2562
+ it('catches errors thrown while detaching refs', () => {
2563
+ class LocalErrorBoundary extends React.Component {
2564
+ state = {error: null};
2565
+ static getDerivedStateFromError(error) {
2566
+ Scheduler.unstable_yieldValue(
2567
+ `ErrorBoundary static getDerivedStateFromError`,
2568
+ );
2569
+ return {error};
2570
+ }
2571
+ render() {
2572
+ const {children, id, fallbackID} = this.props;
2573
+ const {error} = this.state;
2574
+ if (error) {
2575
+ Scheduler.unstable_yieldValue(`${id} render error`);
2576
+ return <Component id={fallbackID} />;
2577
+ }
2578
+ Scheduler.unstable_yieldValue(`${id} render success`);
2579
+ return children || null;
2580
+ }
2581
+ }
2582
+
2583
+ class Component extends React.Component {
2584
+ render() {
2585
+ const {id} = this.props;
2586
+ Scheduler.unstable_yieldValue('Component render ' + id);
2587
+ return id;
2588
+ }
2589
+ }
2590
+
2591
+ class LocalBrokenCallbackRef extends React.Component {
2592
+ _ref = ref => {
2593
+ Scheduler.unstable_yieldValue('LocalBrokenCallbackRef ref ' + !!ref);
2594
+ if (ref === null) {
2595
+ throw Error('Expected');
2596
+ }
2597
+ };
2598
+
2599
+ render() {
2600
+ Scheduler.unstable_yieldValue('LocalBrokenCallbackRef render');
2601
+ return <div ref={this._ref}>ref</div>;
2602
+ }
2603
+ }
2604
+
2605
+ const container = document.createElement('div');
2606
+
2607
+ ReactDOM.render(
2608
+ <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2609
+ <Component id="sibling" />
2610
+ <LocalErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2611
+ <LocalBrokenCallbackRef />
2612
+ </LocalErrorBoundary>
2613
+ </LocalErrorBoundary>,
2614
+ container,
2615
+ );
2616
+
2617
+ expect(container.firstChild.textContent).toBe('sibling');
2618
+ expect(container.lastChild.textContent).toBe('ref');
2619
+ expect(Scheduler).toHaveYielded([
2620
+ 'OuterBoundary render success',
2621
+ 'Component render sibling',
2622
+ 'InnerBoundary render success',
2623
+ 'LocalBrokenCallbackRef render',
2624
+ 'LocalBrokenCallbackRef ref true',
2625
+ ]);
2626
+
2627
+ ReactDOM.render(
2628
+ <LocalErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2629
+ <Component id="sibling" />
2630
+ </LocalErrorBoundary>,
2631
+ container,
2632
+ );
2633
+
2634
+ // React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2635
+ expect(container.firstChild.textContent).toBe('OuterFallback');
2636
+ expect(container.lastChild.textContent).toBe('OuterFallback');
2637
+ expect(Scheduler).toHaveYielded([
2638
+ 'OuterBoundary render success',
2639
+ 'Component render sibling',
2640
+ 'LocalBrokenCallbackRef ref false',
2641
+ 'ErrorBoundary static getDerivedStateFromError',
2642
+ 'OuterBoundary render error',
2643
+ 'Component render OuterFallback',
2644
+ ]);
2645
+ });
2646
});
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+144
-52
@@ -177,7 +177,11 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
177
};
178
179
// Capture errors so they don't interrupt unmounting.
180
-function safelyCallComponentWillUnmount(current: Fiber, instance: any) {
180
+function safelyCallComponentWillUnmount(
181
+ current: Fiber,
182
+ nearestMountedAncestor: Fiber | null,
183
+ instance: any,
184
+) {
185
if (__DEV__) {
186
invokeGuardedCallback(
187
null,
@@ -188,18 +192,18 @@ function safelyCallComponentWillUnmount(current: Fiber, instance: any) {
192
);
193
if (hasCaughtError()) {
194
const unmountError = clearCaughtError();
191
- captureCommitPhaseError(current, unmountError);
195
+ captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
196
}
197
} else {
198
try {
199
callComponentWillUnmountWithTimer(current, instance);
200
} catch (unmountError) {
197
- captureCommitPhaseError(current, unmountError);
201
+ captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
202
}
203
}
204
}
205
202
-function safelyDetachRef(current: Fiber) {
206
+function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
207
const ref = current.ref;
208
if (ref !== null) {
209
if (typeof ref === 'function') {
@@ -218,7 +222,7 @@ function safelyDetachRef(current: Fiber) {
222
223
if (hasCaughtError()) {
224
const refError = clearCaughtError();
221
- captureCommitPhaseError(current, refError);
225
+ captureCommitPhaseError(current, nearestMountedAncestor, refError);
226
}
227
} else {
228
try {
@@ -237,7 +241,7 @@ function safelyDetachRef(current: Fiber) {
241
ref(null);
242
}
243
} catch (refError) {
240
- captureCommitPhaseError(current, refError);
244
+ captureCommitPhaseError(current, nearestMountedAncestor, refError);
245
}
246
}
247
} else {
@@ -246,18 +250,22 @@ function safelyDetachRef(current: Fiber) {
250
}
251
}
252
249
-function safelyCallDestroy(current: Fiber, destroy: () => void) {
253
+function safelyCallDestroy(
254
+ current: Fiber,
255
+ nearestMountedAncestor: Fiber | null,
256
+ destroy: () => void,
257
+) {
258
if (__DEV__) {
259
invokeGuardedCallback(null, destroy, null);
260
if (hasCaughtError()) {
261
const error = clearCaughtError();
254
- captureCommitPhaseError(current, error);
262
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
263
}
264
} else {
265
try {
266
destroy();
267
} catch (error) {
260
- captureCommitPhaseError(current, error);
268
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
269
}
270
}
271
}
@@ -321,14 +329,14 @@ function commitBeforeMutationEffects_complete() {
329
);
330
if (hasCaughtError()) {
331
const error = clearCaughtError();
324
- captureCommitPhaseError(fiber, error);
332
+ captureCommitPhaseError(fiber, fiber.return, error);
333
}
334
resetCurrentDebugFiberInDEV();
335
} else {
336
try {
337
commitBeforeMutationEffectsOnFiber(fiber);
338
} catch (error) {
331
- captureCommitPhaseError(fiber, error);
339
+ captureCommitPhaseError(fiber, fiber.return, error);
340
}
341
}
342
@@ -462,7 +470,11 @@ function commitBeforeMutationEffectsDeletion(deletion: Fiber) {
470
}
471
}
472
465
-function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
473
+function commitHookEffectListUnmount(
474
+ flags: HookFlags,
475
+ finishedWork: Fiber,
476
+ nearestMountedAncestor: Fiber | null,
477
+) {
478
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
479
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
480
if (lastEffect !== null) {
@@ -474,7 +486,7 @@ function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
486
const destroy = effect.destroy;
487
effect.destroy = undefined;
488
if (destroy !== undefined) {
477
- safelyCallDestroy(finishedWork, destroy);
489
+ safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
490
}
491
}
492
effect = effect.next;
@@ -1049,6 +1061,7 @@ function commitDetachRef(current: Fiber) {
1061
function commitUnmount(
1062
finishedRoot: FiberRoot,
1063
current: Fiber,
1064
+ nearestMountedAncestor: Fiber,
1065
renderPriorityLevel: ReactPriorityLevel,
1066
): void {
1067
onCommitUnmount(current);
@@ -1075,10 +1088,10 @@ function commitUnmount(
1088
current.mode & ProfileMode
1089
) {
1090
startLayoutEffectTimer();
1078
- safelyCallDestroy(current, destroy);
1091
+ safelyCallDestroy(current, nearestMountedAncestor, destroy);
1092
recordLayoutEffectDuration(current);
1093
} else {
1081
- safelyCallDestroy(current, destroy);
1094
+ safelyCallDestroy(current, nearestMountedAncestor, destroy);
1095
}
1096
}
1097
}
@@ -1089,15 +1102,19 @@ function commitUnmount(
1102
return;
1103
}
1104
case ClassComponent: {
1092
- safelyDetachRef(current);
1105
+ safelyDetachRef(current, nearestMountedAncestor);
1106
const instance = current.stateNode;
1107
if (typeof instance.componentWillUnmount === 'function') {
1095
- safelyCallComponentWillUnmount(current, instance);
1108
+ safelyCallComponentWillUnmount(
1109
+ current,
1110
+ nearestMountedAncestor,
1111
+ instance,
1112
+ );
1113
}
1114
return;
1115
}
1116
case HostComponent: {
1100
- safelyDetachRef(current);
1117
+ safelyDetachRef(current, nearestMountedAncestor);
1118
return;
1119
}
1120
case HostPortal: {
@@ -1105,7 +1122,12 @@ function commitUnmount(
1122
// We are also not using this parent because
1123
// the portal will get pushed immediately.
1124
if (supportsMutation) {
1108
- unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1125
+ unmountHostComponents(
1126
+ finishedRoot,
1127
+ current,
1128
+ nearestMountedAncestor,
1129
+ renderPriorityLevel,
1130
+ );
1131
} else if (supportsPersistence) {
1132
emptyPortalContainer(current);
1133
}
@@ -1135,7 +1157,7 @@ function commitUnmount(
1157
}
1158
case ScopeComponent: {
1159
if (enableScopeAPI) {
1138
- safelyDetachRef(current);
1160
+ safelyDetachRef(current, nearestMountedAncestor);
1161
}
1162
return;
1163
}
@@ -1145,6 +1167,7 @@ function commitUnmount(
1167
function commitNestedUnmounts(
1168
finishedRoot: FiberRoot,
1169
root: Fiber,
1170
+ nearestMountedAncestor: Fiber,
1171
renderPriorityLevel: ReactPriorityLevel,
1172
): void {
1173
// While we're inside a removed host node we don't want to call
@@ -1154,7 +1177,12 @@ function commitNestedUnmounts(
1177
// we do an inner loop while we're still inside the host node.
1178
let node: Fiber = root;
1179
while (true) {
1157
- commitUnmount(finishedRoot, node, renderPriorityLevel);
1180
+ commitUnmount(
1181
+ finishedRoot,
1182
+ node,
1183
+ nearestMountedAncestor,
1184
+ renderPriorityLevel,
1185
+ );
1186
// Visit children because they may contain more composite or host nodes.
1187
// Skip portals because commitUnmount() currently visits them recursively.
1188
if (
@@ -1455,6 +1483,7 @@ function insertOrAppendPlacementNode(
1483
function unmountHostComponents(
1484
finishedRoot: FiberRoot,
1485
current: Fiber,
1486
+ nearestMountedAncestor: Fiber,
1487
renderPriorityLevel: ReactPriorityLevel,
1488
): void {
1489
// We only have the top Fiber that was deleted but we need to recurse down its
@@ -1504,7 +1533,12 @@ function unmountHostComponents(
1533
}
1534
1535
if (node.tag === HostComponent || node.tag === HostText) {
1507
- commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1536
+ commitNestedUnmounts(
1537
+ finishedRoot,
1538
+ node,
1539
+ nearestMountedAncestor,
1540
+ renderPriorityLevel,
1541
+ );
1542
// After all the children have unmounted, it is now safe to remove the
1543
// node from the tree.
1544
if (currentParentIsContainer) {
@@ -1521,7 +1555,12 @@ function unmountHostComponents(
1555
// Don't visit children because we already visited them.
1556
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
1557
const fundamentalNode = node.stateNode.instance;
1524
- commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1558
+ commitNestedUnmounts(
1559
+ finishedRoot,
1560
+ node,
1561
+ nearestMountedAncestor,
1562
+ renderPriorityLevel,
1563
+ );
1564
// After all the children have unmounted, it is now safe to remove the
1565
// node from the tree.
1566
if (currentParentIsContainer) {
@@ -1573,7 +1612,12 @@ function unmountHostComponents(
1612
continue;
1613
}
1614
} else {
1576
- commitUnmount(finishedRoot, node, renderPriorityLevel);
1615
+ commitUnmount(
1616
+ finishedRoot,
1617
+ node,
1618
+ nearestMountedAncestor,
1619
+ renderPriorityLevel,
1620
+ );
1621
// Visit children because we may find more host components below.
1622
if (node.child !== null) {
1623
node.child.return = node;
@@ -1603,15 +1647,26 @@ function unmountHostComponents(
1647
function commitDeletion(
1648
finishedRoot: FiberRoot,
1649
current: Fiber,
1650
+ nearestMountedAncestor: Fiber,
1651
renderPriorityLevel: ReactPriorityLevel,
1652
): void {
1653
if (supportsMutation) {
1654
// Recursively delete all host nodes from the parent.
1655
// Detach refs and call componentWillUnmount() on the whole subtree.
1611
- unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1656
+ unmountHostComponents(
1657
+ finishedRoot,
1658
+ current,
1659
+ nearestMountedAncestor,
1660
+ renderPriorityLevel,
1661
+ );
1662
} else {
1663
// Detach refs and call componentWillUnmount() on the whole subtree.
1614
- commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
1664
+ commitNestedUnmounts(
1665
+ finishedRoot,
1666
+ current,
1667
+ nearestMountedAncestor,
1668
+ renderPriorityLevel,
1669
+ );
1670
}
1671
const alternate = current.alternate;
1672
detachFiberMutation(current);
@@ -1642,12 +1697,17 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1697
commitHookEffectListUnmount(
1698
HookLayout | HookHasEffect,
1699
finishedWork,
1700
+ finishedWork.return,
1701
);
1702
} finally {
1703
recordLayoutEffectDuration(finishedWork);
1704
}
1705
} else {
1650
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1706
+ commitHookEffectListUnmount(
1707
+ HookLayout | HookHasEffect,
1708
+ finishedWork,
1709
+ finishedWork.return,
1710
+ );
1711
}
1712
return;
1713
}
@@ -1701,12 +1761,20 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1761
) {
1762
try {
1763
startLayoutEffectTimer();
1704
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1764
+ commitHookEffectListUnmount(
1765
+ HookLayout | HookHasEffect,
1766
+ finishedWork,
1767
+ finishedWork.return,
1768
+ );
1769
} finally {
1770
recordLayoutEffectDuration(finishedWork);
1771
}
1772
} else {
1709
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1773
+ commitHookEffectListUnmount(
1774
+ HookLayout | HookHasEffect,
1775
+ finishedWork,
1776
+ finishedWork.return,
1777
+ );
1778
}
1779
return;
1780
}
@@ -1958,17 +2026,18 @@ function commitMutationEffects_begin(
2026
null,
2027
root,
2028
childToDelete,
2029
+ fiber,
2030
renderPriorityLevel,
2031
);
2032
if (hasCaughtError()) {
2033
const error = clearCaughtError();
1965
- captureCommitPhaseError(childToDelete, error);
2034
+ captureCommitPhaseError(childToDelete, fiber, error);
2035
}
2036
} else {
2037
try {
1969
- commitDeletion(root, childToDelete, renderPriorityLevel);
2038
+ commitDeletion(root, childToDelete, fiber, renderPriorityLevel);
2039
} catch (error) {
1971
- captureCommitPhaseError(childToDelete, error);
2040
+ captureCommitPhaseError(childToDelete, fiber, error);
2041
}
2042
}
2043
}
@@ -2002,14 +2071,14 @@ function commitMutationEffects_complete(
2071
);
2072
if (hasCaughtError()) {
2073
const error = clearCaughtError();
2005
- captureCommitPhaseError(fiber, error);
2074
+ captureCommitPhaseError(fiber, fiber.return, error);
2075
}
2076
resetCurrentDebugFiberInDEV();
2077
} else {
2078
try {
2079
commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel);
2080
} catch (error) {
2012
- captureCommitPhaseError(fiber, error);
2081
+ captureCommitPhaseError(fiber, fiber.return, error);
2082
}
2083
}
2084
@@ -2144,14 +2213,14 @@ function commitLayoutMountEffects_complete(
2213
);
2214
if (hasCaughtError()) {
2215
const error = clearCaughtError();
2147
- captureCommitPhaseError(fiber, error);
2216
+ captureCommitPhaseError(fiber, fiber.return, error);
2217
}
2218
resetCurrentDebugFiberInDEV();
2219
} else {
2220
try {
2221
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
2222
} catch (error) {
2154
- captureCommitPhaseError(fiber, error);
2223
+ captureCommitPhaseError(fiber, fiber.return, error);
2224
}
2225
}
2226
}
@@ -2211,14 +2280,14 @@ function commitPassiveMountEffects_complete(
2280
);
2281
if (hasCaughtError()) {
2282
const error = clearCaughtError();
2214
- captureCommitPhaseError(fiber, error);
2283
+ captureCommitPhaseError(fiber, fiber.return, error);
2284
}
2285
resetCurrentDebugFiberInDEV();
2286
} else {
2287
try {
2288
commitPassiveMountOnFiber(root, fiber);
2289
} catch (error) {
2221
- captureCommitPhaseError(fiber, error);
2290
+ captureCommitPhaseError(fiber, fiber.return, error);
2291
}
2292
}
2293
}
@@ -2282,7 +2351,10 @@ function commitPassiveUnmountEffects_begin() {
2351
for (let i = 0; i < deletions.length; i++) {
2352
const fiberToDelete = deletions[i];
2353
nextEffect = fiberToDelete;
2285
- commitPassiveUnmountEffectsInsideOfDeletedTree_begin(fiberToDelete);
2354
+ commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2355
+ fiberToDelete,
2356
+ fiber,
2357
+ );
2358
2359
// Now that passive effects have been processed, it's safe to detach lingering pointers.
2360
const alternate = fiberToDelete.alternate;
@@ -2343,10 +2415,18 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
2415
finishedWork.mode & ProfileMode
2416
) {
2417
startPassiveEffectTimer();
2346
- commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
2418
+ commitHookEffectListUnmount(
2419
+ HookPassive | HookHasEffect,
2420
+ finishedWork,
2421
+ finishedWork.return,
2422
+ );
2423
recordPassiveEffectDuration(finishedWork);
2424
} else {
2349
- commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
2425
+ commitHookEffectListUnmount(
2426
+ HookPassive | HookHasEffect,
2427
+ finishedWork,
2428
+ finishedWork.return,
2429
+ );
2430
}
2431
break;
2432
}
@@ -2355,6 +2435,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
2435
2436
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2437
deletedSubtreeRoot: Fiber,
2438
+ nearestMountedAncestor: Fiber | null,
2439
) {
2440
while (nextEffect !== null) {
2441
const fiber = nextEffect;
@@ -2362,7 +2443,7 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2443
// Deletion effects fire in parent -> child order
2444
// TODO: Check if fiber has a PassiveStatic flag
2445
setCurrentDebugFiberInDEV(fiber);
2365
- commitPassiveUnmountInsideDeletedTreeOnFiber(fiber);
2446
+ commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
2447
resetCurrentDebugFiberInDEV();
2448
2449
const child = fiber.child;
@@ -2399,7 +2480,10 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
2480
}
2481
}
2482
2402
-function commitPassiveUnmountInsideDeletedTreeOnFiber(current: Fiber): void {
2483
+function commitPassiveUnmountInsideDeletedTreeOnFiber(
2484
+ current: Fiber,
2485
+ nearestMountedAncestor: Fiber | null,
2486
+): void {
2487
switch (current.tag) {
2488
case FunctionComponent:
2489
case ForwardRef:
@@ -2410,10 +2494,18 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(current: Fiber): void {
2494
current.mode & ProfileMode
2495
) {
2496
startPassiveEffectTimer();
2413
- commitHookEffectListUnmount(HookPassive, current);
2497
+ commitHookEffectListUnmount(
2498
+ HookPassive,
2499
+ current,
2500
+ nearestMountedAncestor,
2501
+ );
2502
recordPassiveEffectDuration(current);
2503
} else {
2416
- commitHookEffectListUnmount(HookPassive, current);
2504
+ commitHookEffectListUnmount(
2505
+ HookPassive,
2506
+ current,
2507
+ nearestMountedAncestor,
2508
+ );
2509
}
2510
break;
2511
}
@@ -2454,7 +2546,7 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
2546
);
2547
if (hasCaughtError()) {
2548
const mountError = clearCaughtError();
2457
- captureCommitPhaseError(fiber, mountError);
2549
+ captureCommitPhaseError(fiber, fiber.return, mountError);
2550
}
2551
break;
2552
}
@@ -2463,7 +2555,7 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
2555
invokeGuardedCallback(null, instance.componentDidMount, instance);
2556
if (hasCaughtError()) {
2557
const mountError = clearCaughtError();
2466
- captureCommitPhaseError(fiber, mountError);
2558
+ captureCommitPhaseError(fiber, fiber.return, mountError);
2559
}
2560
break;
2561
}
@@ -2488,7 +2580,7 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void {
2580
);
2581
if (hasCaughtError()) {
2582
const mountError = clearCaughtError();
2491
- captureCommitPhaseError(fiber, mountError);
2583
+ captureCommitPhaseError(fiber, fiber.return, mountError);
2584
}
2585
break;
2586
}
@@ -2514,7 +2606,7 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
2606
);
2607
if (hasCaughtError()) {
2608
const unmountError = clearCaughtError();
2517
- captureCommitPhaseError(fiber, unmountError);
2609
+ captureCommitPhaseError(fiber, fiber.return, unmountError);
2610
}
2611
break;
2612
}
@@ -2526,12 +2618,12 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
2618
safelyCallComponentWillUnmount,
2619
null,
2620
fiber,
2529
- instance,
2621
fiber.return,
2622
+ instance,
2623
);
2624
if (hasCaughtError()) {
2625
const unmountError = clearCaughtError();
2534
- captureCommitPhaseError(fiber, unmountError);
2626
+ captureCommitPhaseError(fiber, fiber.return, unmountError);
2627
}
2628
}
2629
break;
@@ -2558,7 +2650,7 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
2650
);
2651
if (hasCaughtError()) {
2652
const unmountError = clearCaughtError();
2561
- captureCommitPhaseError(fiber, unmountError);
2653
+ captureCommitPhaseError(fiber, fiber.return, unmountError);
2654
}
2655
break;
2656
}
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+144
-52
@@ -177,7 +177,11 @@ const callComponentWillUnmountWithTimer = function(current, instance) {
177
};
178
179
// Capture errors so they don't interrupt unmounting.
180
-function safelyCallComponentWillUnmount(current: Fiber, instance: any) {
180
+function safelyCallComponentWillUnmount(
181
+ current: Fiber,
182
+ nearestMountedAncestor: Fiber | null,
183
+ instance: any,
184
+) {
185
if (__DEV__) {
186
invokeGuardedCallback(
187
null,
@@ -188,18 +192,18 @@ function safelyCallComponentWillUnmount(current: Fiber, instance: any) {
192
);
193
if (hasCaughtError()) {
194
const unmountError = clearCaughtError();
191
- captureCommitPhaseError(current, unmountError);
195
+ captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
196
}
197
} else {
198
try {
199
callComponentWillUnmountWithTimer(current, instance);
200
} catch (unmountError) {
197
- captureCommitPhaseError(current, unmountError);
201
+ captureCommitPhaseError(current, nearestMountedAncestor, unmountError);
202
}
203
}
204
}
205
202
-function safelyDetachRef(current: Fiber) {
206
+function safelyDetachRef(current: Fiber, nearestMountedAncestor: Fiber | null) {
207
const ref = current.ref;
208
if (ref !== null) {
209
if (typeof ref === 'function') {
@@ -218,7 +222,7 @@ function safelyDetachRef(current: Fiber) {
222
223
if (hasCaughtError()) {
224
const refError = clearCaughtError();
221
- captureCommitPhaseError(current, refError);
225
+ captureCommitPhaseError(current, nearestMountedAncestor, refError);
226
}
227
} else {
228
try {
@@ -237,7 +241,7 @@ function safelyDetachRef(current: Fiber) {
241
ref(null);
242
}
243
} catch (refError) {
240
- captureCommitPhaseError(current, refError);
244
+ captureCommitPhaseError(current, nearestMountedAncestor, refError);
245
}
246
}
247
} else {
@@ -246,18 +250,22 @@ function safelyDetachRef(current: Fiber) {
250
}
251
}
252
249
-function safelyCallDestroy(current: Fiber, destroy: () => void) {
253
+function safelyCallDestroy(
254
+ current: Fiber,
255
+ nearestMountedAncestor: Fiber | null,
256
+ destroy: () => void,
257
+) {
258
if (__DEV__) {
259
invokeGuardedCallback(null, destroy, null);
260
if (hasCaughtError()) {
261
const error = clearCaughtError();
254
- captureCommitPhaseError(current, error);
262
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
263
}
264
} else {
265
try {
266
destroy();
267
} catch (error) {
260
- captureCommitPhaseError(current, error);
268
+ captureCommitPhaseError(current, nearestMountedAncestor, error);
269
}
270
}
271
}
@@ -321,14 +329,14 @@ function commitBeforeMutationEffects_complete() {
329
);
330
if (hasCaughtError()) {
331
const error = clearCaughtError();
324
- captureCommitPhaseError(fiber, error);
332
+ captureCommitPhaseError(fiber, fiber.return, error);
333
}
334
resetCurrentDebugFiberInDEV();
335
} else {
336
try {
337
commitBeforeMutationEffectsOnFiber(fiber);
338
} catch (error) {
331
- captureCommitPhaseError(fiber, error);
339
+ captureCommitPhaseError(fiber, fiber.return, error);
340
}
341
}
342
@@ -462,7 +470,11 @@ function commitBeforeMutationEffectsDeletion(deletion: Fiber) {
470
}
471
}
472
465
-function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
473
+function commitHookEffectListUnmount(
474
+ flags: HookFlags,
475
+ finishedWork: Fiber,
476
+ nearestMountedAncestor: Fiber | null,
477
+) {
478
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
479
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
480
if (lastEffect !== null) {
@@ -474,7 +486,7 @@ function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
486
const destroy = effect.destroy;
487
effect.destroy = undefined;
488
if (destroy !== undefined) {
477
- safelyCallDestroy(finishedWork, destroy);
489
+ safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
490
}
491
}
492
effect = effect.next;
@@ -1049,6 +1061,7 @@ function commitDetachRef(current: Fiber) {
1061
function commitUnmount(
1062
finishedRoot: FiberRoot,
1063
current: Fiber,
1064
+ nearestMountedAncestor: Fiber,
1065
renderPriorityLevel: ReactPriorityLevel,
1066
): void {
1067
onCommitUnmount(current);
@@ -1075,10 +1088,10 @@ function commitUnmount(
1088
current.mode & ProfileMode
1089
) {
1090
startLayoutEffectTimer();
1078
- safelyCallDestroy(current, destroy);
1091
+ safelyCallDestroy(current, nearestMountedAncestor, destroy);
1092
recordLayoutEffectDuration(current);
1093
} else {
1081
- safelyCallDestroy(current, destroy);
1094
+ safelyCallDestroy(current, nearestMountedAncestor, destroy);
1095
}
1096
}
1097
}
@@ -1089,15 +1102,19 @@ function commitUnmount(
1102
return;
1103
}
1104
case ClassComponent: {
1092
- safelyDetachRef(current);
1105
+ safelyDetachRef(current, nearestMountedAncestor);
1106
const instance = current.stateNode;
1107
if (typeof instance.componentWillUnmount === 'function') {
1095
- safelyCallComponentWillUnmount(current, instance);
1108
+ safelyCallComponentWillUnmount(
1109
+ current,
1110
+ nearestMountedAncestor,
1111
+ instance,
1112
+ );
1113
}
1114
return;
1115
}
1116
case HostComponent: {
1100
- safelyDetachRef(current);
1117
+ safelyDetachRef(current, nearestMountedAncestor);
1118
return;
1119
}
1120
case HostPortal: {
@@ -1105,7 +1122,12 @@ function commitUnmount(
1122
// We are also not using this parent because
1123
// the portal will get pushed immediately.
1124
if (supportsMutation) {
1108
- unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1125
+ unmountHostComponents(
1126
+ finishedRoot,
1127
+ current,
1128
+ nearestMountedAncestor,
1129
+ renderPriorityLevel,
1130
+ );
1131
} else if (supportsPersistence) {
1132
emptyPortalContainer(current);
1133
}
@@ -1135,7 +1157,7 @@ function commitUnmount(
1157
}
1158
case ScopeComponent: {
1159
if (enableScopeAPI) {
1138
- safelyDetachRef(current);
1160
+ safelyDetachRef(current, nearestMountedAncestor);
1161
}
1162
return;
1163
}
@@ -1145,6 +1167,7 @@ function commitUnmount(
1167
function commitNestedUnmounts(
1168
finishedRoot: FiberRoot,
1169
root: Fiber,
1170
+ nearestMountedAncestor: Fiber,
1171
renderPriorityLevel: ReactPriorityLevel,
1172
): void {
1173
// While we're inside a removed host node we don't want to call
@@ -1154,7 +1177,12 @@ function commitNestedUnmounts(
1177
// we do an inner loop while we're still inside the host node.
1178
let node: Fiber = root;
1179
while (true) {
1157
- commitUnmount(finishedRoot, node, renderPriorityLevel);
1180
+ commitUnmount(
1181
+ finishedRoot,
1182
+ node,
1183
+ nearestMountedAncestor,
1184
+ renderPriorityLevel,
1185
+ );
1186
// Visit children because they may contain more composite or host nodes.
1187
// Skip portals because commitUnmount() currently visits them recursively.
1188
if (
@@ -1455,6 +1483,7 @@ function insertOrAppendPlacementNode(
1483
function unmountHostComponents(
1484
finishedRoot: FiberRoot,
1485
current: Fiber,
1486
+ nearestMountedAncestor: Fiber,
1487
renderPriorityLevel: ReactPriorityLevel,
1488
): void {
1489
// We only have the top Fiber that was deleted but we need to recurse down its
@@ -1504,7 +1533,12 @@ function unmountHostComponents(
1533
}
1534
1535
if (node.tag === HostComponent || node.tag === HostText) {
1507
- commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1536
+ commitNestedUnmounts(
1537
+ finishedRoot,
1538
+ node,
1539
+ nearestMountedAncestor,
1540
+ renderPriorityLevel,
1541
+ );
1542
// After all the children have unmounted, it is now safe to remove the
1543
// node from the tree.
1544
if (currentParentIsContainer) {
@@ -1521,7 +1555,12 @@ function unmountHostComponents(
1555
// Don't visit children because we already visited them.
1556
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
1557
const fundamentalNode = node.stateNode.instance;
1524
- commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1558
+ commitNestedUnmounts(
1559
+ finishedRoot,
1560
+ node,
1561
+ nearestMountedAncestor,
1562
+ renderPriorityLevel,
1563
+ );
1564
// After all the children have unmounted, it is now safe to remove the
1565
// node from the tree.
1566
if (currentParentIsContainer) {
@@ -1573,7 +1612,12 @@ function unmountHostComponents(
1612
continue;
1613
}
1614
} else {
1576
- commitUnmount(finishedRoot, node, renderPriorityLevel);
1615
+ commitUnmount(
1616
+ finishedRoot,
1617
+ node,
1618
+ nearestMountedAncestor,
1619
+ renderPriorityLevel,
1620
+ );
1621
// Visit children because we may find more host components below.
1622
if (node.child !== null) {
1623
node.child.return = node;
@@ -1603,15 +1647,26 @@ function unmountHostComponents(
1647
function commitDeletion(
1648
finishedRoot: FiberRoot,
1649
current: Fiber,
1650
+ nearestMountedAncestor: Fiber,
1651
renderPriorityLevel: ReactPriorityLevel,
1652
): void {
1653
if (supportsMutation) {
1654
// Recursively delete all host nodes from the parent.
1655
// Detach refs and call componentWillUnmount() on the whole subtree.
1611
- unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1656
+ unmountHostComponents(
1657
+ finishedRoot,
1658
+ current,
1659
+ nearestMountedAncestor,
1660
+ renderPriorityLevel,
1661
+ );
1662
} else {
1663
// Detach refs and call componentWillUnmount() on the whole subtree.
1614
- commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
1664
+ commitNestedUnmounts(
1665
+ finishedRoot,
1666
+ current,
1667
+ nearestMountedAncestor,
1668
+ renderPriorityLevel,
1669
+ );
1670
}
1671
const alternate = current.alternate;
1672
detachFiberMutation(current);
@@ -1642,12 +1697,17 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1697
commitHookEffectListUnmount(
1698
HookLayout | HookHasEffect,
1699
finishedWork,
1700
+ finishedWork.return,
1701
);
1702
} finally {
1703
recordLayoutEffectDuration(finishedWork);
1704
}
1705
} else {
1650
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1706
+ commitHookEffectListUnmount(
1707
+ HookLayout | HookHasEffect,
1708
+ finishedWork,
1709
+ finishedWork.return,
1710
+ );
1711
}
1712
return;
1713
}
@@ -1701,12 +1761,20 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1761
) {
1762
try {
1763
startLayoutEffectTimer();
1704
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1764
+ commitHookEffectListUnmount(
1765
+ HookLayout | HookHasEffect,
1766
+ finishedWork,
1767
+ finishedWork.return,
1768
+ );
1769
} finally {
1770
recordLayoutEffectDuration(finishedWork);
1771
}
1772
} else {
1709
- commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
1773
+ commitHookEffectListUnmount(
1774
+ HookLayout | HookHasEffect,
1775
+ finishedWork,
1776
+ finishedWork.return,
1777
+ );
1778
}
1779
return;
1780
}
@@ -1958,17 +2026,18 @@ function commitMutationEffects_begin(
2026
null,
2027
root,
2028
childToDelete,
2029
+ fiber,
2030
renderPriorityLevel,
2031
);
2032
if (hasCaughtError()) {
2033
const error = clearCaughtError();
1965
- captureCommitPhaseError(childToDelete, error);
2034
+ captureCommitPhaseError(childToDelete, fiber, error);
2035
}
2036
} else {
2037
try {
1969
- commitDeletion(root, childToDelete, renderPriorityLevel);
2038
+ commitDeletion(root, childToDelete, fiber, renderPriorityLevel);
2039
} catch (error) {
1971
- captureCommitPhaseError(childToDelete, error);
2040
+ captureCommitPhaseError(childToDelete, fiber, error);
2041
}
2042
}
2043
}
@@ -2002,14 +2071,14 @@ function commitMutationEffects_complete(
2071
);
2072
if (hasCaughtError()) {
2073
const error = clearCaughtError();
2005
- captureCommitPhaseError(fiber, error);
2074
+ captureCommitPhaseError(fiber, fiber.return, error);
2075
}
2076
resetCurrentDebugFiberInDEV();
2077
} else {
2078
try {
2079
commitMutationEffectsOnFiber(fiber, root, renderPriorityLevel);
2080
} catch (error) {
2012
- captureCommitPhaseError(fiber, error);
2081
+ captureCommitPhaseError(fiber, fiber.return, error);
2082
}
2083
}
2084
@@ -2144,14 +2213,14 @@ function commitLayoutMountEffects_complete(
2213
);
2214
if (hasCaughtError()) {
2215
const error = clearCaughtError();
2147
- captureCommitPhaseError(fiber, error);
2216
+ captureCommitPhaseError(fiber, fiber.return, error);
2217
}
2218
resetCurrentDebugFiberInDEV();
2219
} else {
2220
try {
2221
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
2222
} catch (error) {
2154
- captureCommitPhaseError(fiber, error);
2223
+ captureCommitPhaseError(fiber, fiber.return, error);
2224
}
2225
}
2226
}
@@ -2211,14 +2280,14 @@ function commitPassiveMountEffects_complete(
2280
);
2281
if (hasCaughtError()) {
2282
const error = clearCaughtError();
2214
- captureCommitPhaseError(fiber, error);
2283
+ captureCommitPhaseError(fiber, fiber.return, error);
2284
}
2285
resetCurrentDebugFiberInDEV();
2286
} else {
2287
try {
2288
commitPassiveMountOnFiber(root, fiber);
2289
} catch (error) {
2221
- captureCommitPhaseError(fiber, error);
2290
+ captureCommitPhaseError(fiber, fiber.return, error);
2291
}
2292
}
2293
}
@@ -2282,7 +2351,10 @@ function commitPassiveUnmountEffects_begin() {
2351
for (let i = 0; i < deletions.length; i++) {
2352
const fiberToDelete = deletions[i];
2353
nextEffect = fiberToDelete;
2285
- commitPassiveUnmountEffectsInsideOfDeletedTree_begin(fiberToDelete);
2354
+ commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2355
+ fiberToDelete,
2356
+ fiber,
2357
+ );
2358
2359
// Now that passive effects have been processed, it's safe to detach lingering pointers.
2360
const alternate = fiberToDelete.alternate;
@@ -2343,10 +2415,18 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
2415
finishedWork.mode & ProfileMode
2416
) {
2417
startPassiveEffectTimer();
2346
- commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
2418
+ commitHookEffectListUnmount(
2419
+ HookPassive | HookHasEffect,
2420
+ finishedWork,
2421
+ finishedWork.return,
2422
+ );
2423
recordPassiveEffectDuration(finishedWork);
2424
} else {
2349
- commitHookEffectListUnmount(HookPassive | HookHasEffect, finishedWork);
2425
+ commitHookEffectListUnmount(
2426
+ HookPassive | HookHasEffect,
2427
+ finishedWork,
2428
+ finishedWork.return,
2429
+ );
2430
}
2431
break;
2432
}
@@ -2355,6 +2435,7 @@ function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
2435
2436
function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2437
deletedSubtreeRoot: Fiber,
2438
+ nearestMountedAncestor: Fiber | null,
2439
) {
2440
while (nextEffect !== null) {
2441
const fiber = nextEffect;
@@ -2362,7 +2443,7 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_begin(
2443
// Deletion effects fire in parent -> child order
2444
// TODO: Check if fiber has a PassiveStatic flag
2445
setCurrentDebugFiberInDEV(fiber);
2365
- commitPassiveUnmountInsideDeletedTreeOnFiber(fiber);
2446
+ commitPassiveUnmountInsideDeletedTreeOnFiber(fiber, nearestMountedAncestor);
2447
resetCurrentDebugFiberInDEV();
2448
2449
const child = fiber.child;
@@ -2399,7 +2480,10 @@ function commitPassiveUnmountEffectsInsideOfDeletedTree_complete(
2480
}
2481
}
2482
2402
-function commitPassiveUnmountInsideDeletedTreeOnFiber(current: Fiber): void {
2483
+function commitPassiveUnmountInsideDeletedTreeOnFiber(
2484
+ current: Fiber,
2485
+ nearestMountedAncestor: Fiber | null,
2486
+): void {
2487
switch (current.tag) {
2488
case FunctionComponent:
2489
case ForwardRef:
@@ -2410,10 +2494,18 @@ function commitPassiveUnmountInsideDeletedTreeOnFiber(current: Fiber): void {
2494
current.mode & ProfileMode
2495
) {
2496
startPassiveEffectTimer();
2413
- commitHookEffectListUnmount(HookPassive, current);
2497
+ commitHookEffectListUnmount(
2498
+ HookPassive,
2499
+ current,
2500
+ nearestMountedAncestor,
2501
+ );
2502
recordPassiveEffectDuration(current);
2503
} else {
2416
- commitHookEffectListUnmount(HookPassive, current);
2504
+ commitHookEffectListUnmount(
2505
+ HookPassive,
2506
+ current,
2507
+ nearestMountedAncestor,
2508
+ );
2509
}
2510
break;
2511
}
@@ -2454,7 +2546,7 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
2546
);
2547
if (hasCaughtError()) {
2548
const mountError = clearCaughtError();
2457
- captureCommitPhaseError(fiber, mountError);
2549
+ captureCommitPhaseError(fiber, fiber.return, mountError);
2550
}
2551
break;
2552
}
@@ -2463,7 +2555,7 @@ function invokeLayoutEffectMountInDEV(fiber: Fiber): void {
2555
invokeGuardedCallback(null, instance.componentDidMount, instance);
2556
if (hasCaughtError()) {
2557
const mountError = clearCaughtError();
2466
- captureCommitPhaseError(fiber, mountError);
2558
+ captureCommitPhaseError(fiber, fiber.return, mountError);
2559
}
2560
break;
2561
}
@@ -2488,7 +2580,7 @@ function invokePassiveEffectMountInDEV(fiber: Fiber): void {
2580
);
2581
if (hasCaughtError()) {
2582
const mountError = clearCaughtError();
2491
- captureCommitPhaseError(fiber, mountError);
2583
+ captureCommitPhaseError(fiber, fiber.return, mountError);
2584
}
2585
break;
2586
}
@@ -2514,7 +2606,7 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
2606
);
2607
if (hasCaughtError()) {
2608
const unmountError = clearCaughtError();
2517
- captureCommitPhaseError(fiber, unmountError);
2609
+ captureCommitPhaseError(fiber, fiber.return, unmountError);
2610
}
2611
break;
2612
}
@@ -2526,12 +2618,12 @@ function invokeLayoutEffectUnmountInDEV(fiber: Fiber): void {
2618
safelyCallComponentWillUnmount,
2619
null,
2620
fiber,
2529
- instance,
2621
fiber.return,
2622
+ instance,
2623
);
2624
if (hasCaughtError()) {
2625
const unmountError = clearCaughtError();
2534
- captureCommitPhaseError(fiber, unmountError);
2626
+ captureCommitPhaseError(fiber, fiber.return, unmountError);
2627
}
2628
}
2629
break;
@@ -2558,7 +2650,7 @@ function invokePassiveEffectUnmountInDEV(fiber: Fiber): void {
2650
);
2651
if (hasCaughtError()) {
2652
const unmountError = clearCaughtError();
2561
- captureCommitPhaseError(fiber, unmountError);
2653
+ captureCommitPhaseError(fiber, fiber.return, unmountError);
2654
}
2655
break;
2656
}
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+13
-2
@@ -33,6 +33,7 @@ import {
33
enableSchedulingProfiler,
34
disableSchedulerTimeoutInWorkLoop,
35
enableDoubleInvokingEffects,
36
+ skipUnmountedBoundaries,
37
} from 'shared/ReactFeatureFlags';
38
import ReactSharedInternals from 'shared/ReactSharedInternals';
39
import invariant from 'shared/invariant';
@@ -2312,7 +2313,11 @@ function captureCommitPhaseErrorOnRoot(
2313
}
2314
}
2315
2315
-export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2316
+export function captureCommitPhaseError(
2317
+ sourceFiber: Fiber,
2318
+ nearestMountedAncestor: Fiber | null,
2319
+ error: mixed,
2320
+) {
2321
if (sourceFiber.tag === HostRoot) {
2322
// Error was thrown at the root. There is no parent, so the root
2323
// itself should capture it.
@@ -2320,7 +2325,13 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2325
return;
2326
}
2327
2323
- let fiber = sourceFiber.return;
2328
+ let fiber = null;
2329
+ if (skipUnmountedBoundaries) {
2330
+ fiber = nearestMountedAncestor;
2331
+ } else {
2332
+ fiber = sourceFiber.return;
2333
+ }
2334
+
2335
while (fiber !== null) {
2336
if (fiber.tag === HostRoot) {
2337
captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+13
-2
@@ -33,6 +33,7 @@ import {
33
enableSchedulingProfiler,
34
disableSchedulerTimeoutInWorkLoop,
35
enableDoubleInvokingEffects,
36
+ skipUnmountedBoundaries,
37
} from 'shared/ReactFeatureFlags';
38
import ReactSharedInternals from 'shared/ReactSharedInternals';
39
import invariant from 'shared/invariant';
@@ -2292,7 +2293,11 @@ function captureCommitPhaseErrorOnRoot(
2293
}
2294
}
2295
2295
-export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2296
+export function captureCommitPhaseError(
2297
+ sourceFiber: Fiber,
2298
+ nearestMountedAncestor: Fiber | null,
2299
+ error: mixed,
2300
+) {
2301
if (sourceFiber.tag === HostRoot) {
2302
// Error was thrown at the root. There is no parent, so the root
2303
// itself should capture it.
@@ -2300,7 +2305,13 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2305
return;
2306
}
2307
2303
- let fiber = sourceFiber.return;
2308
+ let fiber = null;
2309
+ if (skipUnmountedBoundaries) {
2310
+ fiber = nearestMountedAncestor;
2311
+ } else {
2312
+ fiber = sourceFiber.return;
2313
+ }
2314
+
2315
while (fiber !== null) {
2316
if (fiber.tag === HostRoot) {
2317
captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+283
@@ -2358,6 +2358,210 @@ describe('ReactHooksWithNoopRenderer', () => {
2358
expect(ReactNoop.getChildren()).toEqual([]);
2359
});
2360
2361
+ describe('errors thrown in passive destroy function within unmounted trees', () => {
2362
+ let BrokenUseEffectCleanup;
2363
+ let ErrorBoundary;
2364
+ let LogOnlyErrorBoundary;
2365
+
2366
+ beforeEach(() => {
2367
+ BrokenUseEffectCleanup = function() {
2368
+ useEffect(() => {
2369
+ Scheduler.unstable_yieldValue('BrokenUseEffectCleanup useEffect');
2370
+ return () => {
2371
+ Scheduler.unstable_yieldValue(
2372
+ 'BrokenUseEffectCleanup useEffect destroy',
2373
+ );
2374
+ throw new Error('Expected error');
2375
+ };
2376
+ }, []);
2377
+
2378
+ return 'inner child';
2379
+ };
2380
+
2381
+ ErrorBoundary = class extends React.Component {
2382
+ state = {error: null};
2383
+ static getDerivedStateFromError(error) {
2384
+ Scheduler.unstable_yieldValue(
2385
+ `ErrorBoundary static getDerivedStateFromError`,
2386
+ );
2387
+ return {error};
2388
+ }
2389
+ componentDidCatch(error, info) {
2390
+ Scheduler.unstable_yieldValue(`ErrorBoundary componentDidCatch`);
2391
+ }
2392
+ render() {
2393
+ if (this.state.error) {
2394
+ Scheduler.unstable_yieldValue('ErrorBoundary render error');
2395
+ return <span prop="ErrorBoundary fallback" />;
2396
+ }
2397
+ Scheduler.unstable_yieldValue('ErrorBoundary render success');
2398
+ return this.props.children || null;
2399
+ }
2400
+ };
2401
+
2402
+ LogOnlyErrorBoundary = class extends React.Component {
2403
+ componentDidCatch(error, info) {
2404
+ Scheduler.unstable_yieldValue(
2405
+ `LogOnlyErrorBoundary componentDidCatch`,
2406
+ );
2407
+ }
2408
+ render() {
2409
+ Scheduler.unstable_yieldValue(`LogOnlyErrorBoundary render`);
2410
+ return this.props.children || null;
2411
+ }
2412
+ };
2413
+ });
2414
+
2415
+ // @gate skipUnmountedBoundaries
2416
+ it('should use the nearest still-mounted boundary if there are no unmounted boundaries', () => {
2417
+ act(() => {
2418
+ ReactNoop.render(
2419
+ <LogOnlyErrorBoundary>
2420
+ <BrokenUseEffectCleanup />
2421
+ </LogOnlyErrorBoundary>,
2422
+ );
2423
+ });
2424
+
2425
+ expect(Scheduler).toHaveYielded([
2426
+ 'LogOnlyErrorBoundary render',
2427
+ 'BrokenUseEffectCleanup useEffect',
2428
+ ]);
2429
+
2430
+ act(() => {
2431
+ ReactNoop.render(<LogOnlyErrorBoundary />);
2432
+ });
2433
+
2434
+ expect(Scheduler).toHaveYielded([
2435
+ 'LogOnlyErrorBoundary render',
2436
+ 'BrokenUseEffectCleanup useEffect destroy',
2437
+ 'LogOnlyErrorBoundary componentDidCatch',
2438
+ ]);
2439
+ });
2440
+
2441
+ // @gate skipUnmountedBoundaries
2442
+ it('should skip unmounted boundaries and use the nearest still-mounted boundary', () => {
2443
+ function Conditional({showChildren}) {
2444
+ if (showChildren) {
2445
+ return (
2446
+ <ErrorBoundary>
2447
+ <BrokenUseEffectCleanup />
2448
+ </ErrorBoundary>
2449
+ );
2450
+ } else {
2451
+ return null;
2452
+ }
2453
+ }
2454
+
2455
+ act(() => {
2456
+ ReactNoop.render(
2457
+ <LogOnlyErrorBoundary>
2458
+ <Conditional showChildren={true} />
2459
+ </LogOnlyErrorBoundary>,
2460
+ );
2461
+ });
2462
+
2463
+ expect(Scheduler).toHaveYielded([
2464
+ 'LogOnlyErrorBoundary render',
2465
+ 'ErrorBoundary render success',
2466
+ 'BrokenUseEffectCleanup useEffect',
2467
+ ]);
2468
+
2469
+ act(() => {
2470
+ ReactNoop.render(
2471
+ <LogOnlyErrorBoundary>
2472
+ <Conditional showChildren={false} />
2473
+ </LogOnlyErrorBoundary>,
2474
+ );
2475
+ });
2476
+
2477
+ expect(Scheduler).toHaveYielded([
2478
+ 'LogOnlyErrorBoundary render',
2479
+ 'BrokenUseEffectCleanup useEffect destroy',
2480
+ 'LogOnlyErrorBoundary componentDidCatch',
2481
+ ]);
2482
+ });
2483
+
2484
+ // @gate skipUnmountedBoundaries
2485
+ it('should call getDerivedStateFromError in the nearest still-mounted boundary', () => {
2486
+ function Conditional({showChildren}) {
2487
+ if (showChildren) {
2488
+ return <BrokenUseEffectCleanup />;
2489
+ } else {
2490
+ return null;
2491
+ }
2492
+ }
2493
+
2494
+ act(() => {
2495
+ ReactNoop.render(
2496
+ <ErrorBoundary>
2497
+ <Conditional showChildren={true} />
2498
+ </ErrorBoundary>,
2499
+ );
2500
+ });
2501
+
2502
+ expect(Scheduler).toHaveYielded([
2503
+ 'ErrorBoundary render success',
2504
+ 'BrokenUseEffectCleanup useEffect',
2505
+ ]);
2506
+
2507
+ act(() => {
2508
+ ReactNoop.render(
2509
+ <ErrorBoundary>
2510
+ <Conditional showChildren={false} />
2511
+ </ErrorBoundary>,
2512
+ );
2513
+ });
2514
+
2515
+ expect(Scheduler).toHaveYielded([
2516
+ 'ErrorBoundary render success',
2517
+ 'BrokenUseEffectCleanup useEffect destroy',
2518
+ 'ErrorBoundary static getDerivedStateFromError',
2519
+ 'ErrorBoundary render error',
2520
+ 'ErrorBoundary componentDidCatch',
2521
+ ]);
2522
+
2523
+ expect(ReactNoop.getChildren()).toEqual([
2524
+ span('ErrorBoundary fallback'),
2525
+ ]);
2526
+ });
2527
+
2528
+ // @gate skipUnmountedBoundaries
2529
+ it('should rethrow error if there are no still-mounted boundaries', () => {
2530
+ function Conditional({showChildren}) {
2531
+ if (showChildren) {
2532
+ return (
2533
+ <ErrorBoundary>
2534
+ <BrokenUseEffectCleanup />
2535
+ </ErrorBoundary>
2536
+ );
2537
+ } else {
2538
+ return null;
2539
+ }
2540
+ }
2541
+
2542
+ act(() => {
2543
+ ReactNoop.render(<Conditional showChildren={true} />);
2544
+ });
2545
+
2546
+ expect(Scheduler).toHaveYielded([
2547
+ 'ErrorBoundary render success',
2548
+ 'BrokenUseEffectCleanup useEffect',
2549
+ ]);
2550
+
2551
+ expect(() => {
2552
+ act(() => {
2553
+ ReactNoop.render(<Conditional showChildren={false} />);
2554
+ });
2555
+ }).toThrow('Expected error');
2556
+
2557
+ expect(Scheduler).toHaveYielded([
2558
+ 'BrokenUseEffectCleanup useEffect destroy',
2559
+ ]);
2560
+
2561
+ expect(ReactNoop.getChildren()).toEqual([]);
2562
+ });
2563
+ });
2564
+
2565
it('calls passive effect destroy functions for memoized components', () => {
2566
const Wrapper = ({children}) => children;
2567
function Child() {
@@ -2594,6 +2798,85 @@ describe('ReactHooksWithNoopRenderer', () => {
2798
'Mount normal [current: 1]',
2799
]);
2800
});
2801
+
2802
+ // @gate skipUnmountedBoundaries
2803
+ it('catches errors thrown in useLayoutEffect', () => {
2804
+ class ErrorBoundary extends React.Component {
2805
+ state = {error: null};
2806
+ static getDerivedStateFromError(error) {
2807
+ Scheduler.unstable_yieldValue(
2808
+ `ErrorBoundary static getDerivedStateFromError`,
2809
+ );
2810
+ return {error};
2811
+ }
2812
+ render() {
2813
+ const {children, id, fallbackID} = this.props;
2814
+ const {error} = this.state;
2815
+ if (error) {
2816
+ Scheduler.unstable_yieldValue(`${id} render error`);
2817
+ return <Component id={fallbackID} />;
2818
+ }
2819
+ Scheduler.unstable_yieldValue(`${id} render success`);
2820
+ return children || null;
2821
+ }
2822
+ }
2823
+
2824
+ function Component({id}) {
2825
+ Scheduler.unstable_yieldValue('Component render ' + id);
2826
+ return <span prop={id} />;
2827
+ }
2828
+
2829
+ function BrokenLayoutEffectDestroy() {
2830
+ useLayoutEffect(() => {
2831
+ return () => {
2832
+ Scheduler.unstable_yieldValue(
2833
+ 'BrokenLayoutEffectDestroy useLayoutEffect destroy',
2834
+ );
2835
+ throw Error('Expected');
2836
+ };
2837
+ }, []);
2838
+
2839
+ Scheduler.unstable_yieldValue('BrokenLayoutEffectDestroy render');
2840
+ return <span prop="broken" />;
2841
+ }
2842
+
2843
+ ReactNoop.render(
2844
+ <ErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2845
+ <Component id="sibling" />
2846
+ <ErrorBoundary id="InnerBoundary" fallbackID="InnerFallback">
2847
+ <BrokenLayoutEffectDestroy />
2848
+ </ErrorBoundary>
2849
+ </ErrorBoundary>,
2850
+ );
2851
+
2852
+ expect(Scheduler).toFlushAndYield([
2853
+ 'OuterBoundary render success',
2854
+ 'Component render sibling',
2855
+ 'InnerBoundary render success',
2856
+ 'BrokenLayoutEffectDestroy render',
2857
+ ]);
2858
+ expect(ReactNoop.getChildren()).toEqual([
2859
+ span('sibling'),
2860
+ span('broken'),
2861
+ ]);
2862
+
2863
+ ReactNoop.render(
2864
+ <ErrorBoundary id="OuterBoundary" fallbackID="OuterFallback">
2865
+ <Component id="sibling" />
2866
+ </ErrorBoundary>,
2867
+ );
2868
+
2869
+ // React should skip over the unmounting boundary and find the nearest still-mounted boundary.
2870
+ expect(Scheduler).toFlushAndYield([
2871
+ 'OuterBoundary render success',
2872
+ 'Component render sibling',
2873
+ 'BrokenLayoutEffectDestroy useLayoutEffect destroy',
2874
+ 'ErrorBoundary static getDerivedStateFromError',
2875
+ 'OuterBoundary render error',
2876
+ 'Component render OuterFallback',
2877
+ ]);
2878
+ expect(ReactNoop.getChildren()).toEqual([span('OuterFallback')]);
2879
+ });
2880
});
2881
2882
describe('useCallback', () => {
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+12
-6
@@ -961,6 +961,7 @@ describe('ReactIncrementalErrorHandling', () => {
961
expect(Scheduler).toFlushAndYield(['Foo']);
962
});
963
964
+ // @gate skipUnmountedBoundaries
965
it('should not attempt to recover an unmounting error boundary', () => {
966
class Parent extends React.Component {
967
componentWillUnmount() {
@@ -992,12 +993,17 @@ describe('ReactIncrementalErrorHandling', () => {
993
994
ReactNoop.render(<Parent />);
995
expect(Scheduler).toFlushWithoutYielding();
995
- ReactNoop.render(null);
996
- expect(Scheduler).toFlushAndYield([
997
- // Parent unmounts before the error is thrown.
998
- 'Parent componentWillUnmount',
999
- 'ThrowsOnUnmount componentWillUnmount',
1000
- ]);
996
+
997
+ // Because the error boundary is also unmounting,
998
+ // an error in ThrowsOnUnmount should be rethrown.
999
+ expect(() => {
1000
+ ReactNoop.render(null);
1001
+ expect(Scheduler).toFlushAndYield([
1002
+ 'Parent componentWillUnmount',
1003
+ 'ThrowsOnUnmount componentWillUnmount',
1004
+ ]);
1005
+ }).toThrow('unmount error');
1006
+
1007
ReactNoop.render(<Parent />);
1008
});
1009
packages/shared/ReactFeatureFlags.js
+6
@@ -101,6 +101,12 @@ export const enableNewReconciler = false;
101
102
export const disableNativeComponentFrames = false;
103
104
+// Errors that are thrown while unmounting (or after in the case of passive effects)
105
+// should bypass any error boundaries that are also unmounting (or have unmounted)
106
+// and be handled by the nearest still-mounted boundary.
107
+// If there are no still-mounted boundaries, the errors should be rethrown.
108
+export const skipUnmountedBoundaries = false;
109
+
110
// --------------------------
111
// Future APIs to be deprecated
112
// --------------------------
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -46,6 +46,7 @@ export const enableComponentStackLocations = false;
46
export const enableLegacyFBSupport = false;
47
export const enableFilterEmptyStringAttributesDOM = false;
48
export const disableNativeComponentFrames = false;
49
+export const skipUnmountedBoundaries = false;
50
51
export const enableNewReconciler = false;
52
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -45,6 +45,7 @@ export const enableComponentStackLocations = false;
45
export const enableLegacyFBSupport = false;
46
export const enableFilterEmptyStringAttributesDOM = false;
47
export const disableNativeComponentFrames = false;
48
+export const skipUnmountedBoundaries = false;
49
50
export const enableNewReconciler = false;
51
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -45,6 +45,7 @@ export const enableComponentStackLocations = true;
45
export const enableLegacyFBSupport = false;
46
export const enableFilterEmptyStringAttributesDOM = false;
47
export const disableNativeComponentFrames = false;
48
+export const skipUnmountedBoundaries = false;
49
50
export const enableNewReconciler = false;
51
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -45,6 +45,7 @@ export const enableComponentStackLocations = false;
45
export const enableLegacyFBSupport = false;
46
export const enableFilterEmptyStringAttributesDOM = false;
47
export const disableNativeComponentFrames = false;
48
+export const skipUnmountedBoundaries = false;
49
50
export const enableNewReconciler = false;
51
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -45,6 +45,7 @@ export const enableComponentStackLocations = true;
45
export const enableLegacyFBSupport = false;
46
export const enableFilterEmptyStringAttributesDOM = false;
47
export const disableNativeComponentFrames = false;
48
+export const skipUnmountedBoundaries = false;
49
50
export const enableNewReconciler = false;
51
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -45,6 +45,7 @@ export const enableComponentStackLocations = true;
45
export const enableLegacyFBSupport = false;
46
export const enableFilterEmptyStringAttributesDOM = false;
47
export const disableNativeComponentFrames = false;
48
+export const skipUnmountedBoundaries = false;
49
50
export const enableNewReconciler = false;
51
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -45,6 +45,7 @@ export const enableComponentStackLocations = true;
45
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
46
export const enableFilterEmptyStringAttributesDOM = false;
47
export const disableNativeComponentFrames = false;
48
+export const skipUnmountedBoundaries = true;
49
50
export const enableNewReconciler = false;
51
export const deferRenderPhaseUpdateToNextBatch = true;
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -18,6 +18,7 @@ export const disableInputAttributeSyncing = __VARIANT__;
18
export const enableFilterEmptyStringAttributesDOM = __VARIANT__;
19
export const enableLegacyFBSupport = __VARIANT__;
20
export const decoupleUpdatePriorityFromScheduler = __VARIANT__;
21
+export const skipUnmountedBoundaries = __VARIANT__;
22
23
// Enable this flag to help with concurrent mode debugging.
24
// It logs information to the console about React scheduling, rendering, and commit phases.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -26,6 +26,7 @@ export const {
26
deferRenderPhaseUpdateToNextBatch,
27
decoupleUpdatePriorityFromScheduler,
28
enableDebugTracing,
29
+ skipUnmountedBoundaries,
30
enableDoubleInvokingEffects,
31
enableUseRefAccessWarning,
32
disableNativeComponentFrames,