@samitouri / QOS-React-2 / commits / 895ae67fd3

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,