@samitouri / QOS-React / commits / ffb749c95e

Improve error boundary handling for unmounted subtrees (#19542)

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's behavior varies depending on which reconciler fork is being used. For the old reconciler, React will call componentDidCatch for the nearest unmounted error boundary (if there is one). If there are no unmounted error boundaries, React will still swallow the error because the return pointer has been disconnected, so the normal error handling logic does not know how to traverse the tree to find the nearest still-mounted ancestor. For the new reconciler, 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). Tests have been added for both reconciler variants for now.

Brian Vaughn committed Aug 14, 2020 at 16:46 UTC ffb749c95e0361b3cfbbfc4e1a73bfa2fda0aa93
5 files changed +400 -53
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+40 -16
@@ -1667,16 +1667,28 @@ describe('DOMPluginEventSystem', () => {
1667
1668 function Test() {
1669 React.useEffect(() => {
1670 - setClick1(buttonRef.current, targetListener1);
1671 - setClick2(buttonRef.current, targetListener2);
1672 - setClick3(buttonRef.current, targetListener3);
1673 - setClick4(buttonRef.current, targetListener4);
1670 + const clearClick1 = setClick1(
1671 + buttonRef.current,
1672 + targetListener1,
1673 + );
1674 + const clearClick2 = setClick2(
1675 + buttonRef.current,
1676 + targetListener2,
1677 + );
1678 + const clearClick3 = setClick3(
1679 + buttonRef.current,
1680 + targetListener3,
1681 + );
1682 + const clearClick4 = setClick4(
1683 + buttonRef.current,
1684 + targetListener4,
1685 + );
1686
1687 return () => {
1676 - setClick1();
1677 - setClick2();
1678 - setClick3();
1679 - setClick4();
1688 + clearClick1();
1689 + clearClick2();
1690 + clearClick3();
1691 + clearClick4();
1692 };
1693 });
1694
@@ -1701,16 +1713,28 @@ describe('DOMPluginEventSystem', () => {
1713
1714 function Test2() {
1715 React.useEffect(() => {
1704 - setClick1(buttonRef.current, targetListener1);
1705 - setClick2(buttonRef.current, targetListener2);
1706 - setClick3(buttonRef.current, targetListener3);
1707 - setClick4(buttonRef.current, targetListener4);
1716 + const clearClick1 = setClick1(
1717 + buttonRef.current,
1718 + targetListener1,
1719 + );
1720 + const clearClick2 = setClick2(
1721 + buttonRef.current,
1722 + targetListener2,
1723 + );
1724 + const clearClick3 = setClick3(
1725 + buttonRef.current,
1726 + targetListener3,
1727 + );
1728 + const clearClick4 = setClick4(
1729 + buttonRef.current,
1730 + targetListener4,
1731 + );
1732
1733 return () => {
1710 - setClick1();
1711 - setClick2();
1712 - setClick3();
1713 - setClick4();
1734 + clearClick1();
1735 + clearClick2();
1736 + clearClick3();
1737 + clearClick4();
1738 };
1739 });
1740
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+33 -18
@@ -173,13 +173,13 @@ function safelyCallComponentWillUnmount(current, instance) {
173 );
174 if (hasCaughtError()) {
175 const unmountError = clearCaughtError();
176 - captureCommitPhaseError(current, unmountError);
176 + captureCommitPhaseError(current, current.return, unmountError);
177 }
178 } else {
179 try {
180 callComponentWillUnmountWithTimer(current, instance);
181 } catch (unmountError) {
182 - captureCommitPhaseError(current, unmountError);
182 + captureCommitPhaseError(current, current.return, unmountError);
183 }
184 }
185 }
@@ -192,13 +192,13 @@ function safelyDetachRef(current: Fiber) {
192 invokeGuardedCallback(null, ref, null, null);
193 if (hasCaughtError()) {
194 const refError = clearCaughtError();
195 - captureCommitPhaseError(current, refError);
195 + captureCommitPhaseError(current, current.return, refError);
196 }
197 } else {
198 try {
199 ref(null);
200 } catch (refError) {
201 - captureCommitPhaseError(current, refError);
201 + captureCommitPhaseError(current, current.return, refError);
202 }
203 }
204 } else {
@@ -207,18 +207,22 @@ function safelyDetachRef(current: Fiber) {
207 }
208 }
209
210 -export function safelyCallDestroy(current: Fiber, destroy: () => void) {
210 +export function safelyCallDestroy(
211 + current: Fiber,
212 + nearestMountedAncestor: Fiber | null,
213 + destroy: () => void,
214 +) {
215 if (__DEV__) {
216 invokeGuardedCallback(null, destroy, null);
217 if (hasCaughtError()) {
218 const error = clearCaughtError();
215 - captureCommitPhaseError(current, error);
219 + captureCommitPhaseError(current, nearestMountedAncestor, error);
220 }
221 } else {
222 try {
223 destroy();
224 } catch (error) {
221 - captureCommitPhaseError(current, error);
225 + captureCommitPhaseError(current, nearestMountedAncestor, error);
226 }
227 }
228 }
@@ -337,10 +341,10 @@ function commitHookEffectListUnmount(tag: HookEffectTag, finishedWork: Fiber) {
341
342 // TODO: Remove this duplication.
343 function commitHookEffectListUnmount2(
340 - // Tags to check for when deciding whether to unmount. e.g. to skip over
341 - // layout effects
344 + // Tags to check for when deciding whether to unmount. e.g. to skip over layout effects
345 hookEffectTag: HookEffectTag,
346 fiber: Fiber,
347 + nearestMountedAncestor: Fiber | null,
348 ): void {
349 const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any);
350 const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
@@ -359,10 +363,10 @@ function commitHookEffectListUnmount2(
363 fiber.mode & ProfileMode
364 ) {
365 startPassiveEffectTimer();
362 - safelyCallDestroy(fiber, destroy);
366 + safelyCallDestroy(fiber, nearestMountedAncestor, destroy);
367 recordPassiveEffectDuration(fiber);
368 } else {
365 - safelyCallDestroy(fiber, destroy);
369 + safelyCallDestroy(fiber, nearestMountedAncestor, destroy);
370 }
371 }
372 }
@@ -465,7 +469,7 @@ function commitHookEffectListMount2(fiber: Fiber): void {
469 if (hasCaughtError()) {
470 invariant(fiber !== null, 'Should be working on an effect.');
471 const error = clearCaughtError();
468 - captureCommitPhaseError(fiber, error);
472 + captureCommitPhaseError(fiber, fiber.return, error);
473 }
474 } else {
475 try {
@@ -488,7 +492,7 @@ function commitHookEffectListMount2(fiber: Fiber): void {
492 // The warning refers to useEffect but only applies to useLayoutEffect.
493 } catch (error) {
494 invariant(fiber !== null, 'Should be working on an effect.');
491 - captureCommitPhaseError(fiber, error);
495 + captureCommitPhaseError(fiber, fiber.return, error);
496 }
497 }
498 }
@@ -997,10 +1001,10 @@ function commitUnmount(
1001 current.mode & ProfileMode
1002 ) {
1003 startLayoutEffectTimer();
1000 - safelyCallDestroy(current, destroy);
1004 + safelyCallDestroy(current, current.return, destroy);
1005 recordLayoutEffectDuration(current);
1006 } else {
1003 - safelyCallDestroy(current, destroy);
1007 + safelyCallDestroy(current, current.return, destroy);
1008 }
1009 }
1010 }
@@ -1842,18 +1846,29 @@ function commitPassiveWork(finishedWork: Fiber): void {
1846 case ForwardRef:
1847 case SimpleMemoComponent:
1848 case Block: {
1845 - commitHookEffectListUnmount2(HookPassive | HookHasEffect, finishedWork);
1849 + commitHookEffectListUnmount2(
1850 + HookPassive | HookHasEffect,
1851 + finishedWork,
1852 + finishedWork.return,
1853 + );
1854 }
1855 }
1856 }
1857
1850 -function commitPassiveUnmount(current: Fiber): void {
1858 +function commitPassiveUnmount(
1859 + current: Fiber,
1860 + nearestMountedAncestor: Fiber | null,
1861 +): void {
1862 switch (current.tag) {
1863 case FunctionComponent:
1864 case ForwardRef:
1865 case SimpleMemoComponent:
1866 case Block:
1856 - commitHookEffectListUnmount2(HookPassive, current);
1867 + commitHookEffectListUnmount2(
1868 + HookPassive,
1869 + current,
1870 + nearestMountedAncestor,
1871 + );
1872 }
1873 }
1874
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+21 -13
@@ -2393,14 +2393,14 @@ function commitBeforeMutationEffects(firstChild: Fiber) {
2393 invokeGuardedCallback(null, commitBeforeMutationEffectsImpl, null, fiber);
2394 if (hasCaughtError()) {
2395 const error = clearCaughtError();
2396 - captureCommitPhaseError(fiber, error);
2396 + captureCommitPhaseError(fiber, fiber.return, error);
2397 }
2398 resetCurrentDebugFiberInDEV();
2399 } else {
2400 try {
2401 commitBeforeMutationEffectsImpl(fiber);
2402 } catch (error) {
2403 - captureCommitPhaseError(fiber, error);
2403 + captureCommitPhaseError(fiber, fiber.return, error);
2404 }
2405 }
2406 fiber = fiber.sibling;
@@ -2490,14 +2490,14 @@ function commitMutationEffects(
2490 );
2491 if (hasCaughtError()) {
2492 const error = clearCaughtError();
2493 - captureCommitPhaseError(fiber, error);
2493 + captureCommitPhaseError(fiber, fiber.return, error);
2494 }
2495 resetCurrentDebugFiberInDEV();
2496 } else {
2497 try {
2498 commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
2499 } catch (error) {
2500 - captureCommitPhaseError(fiber, error);
2500 + captureCommitPhaseError(fiber, fiber.return, error);
2501 }
2502 }
2503 fiber = fiber.sibling;
@@ -2593,13 +2593,13 @@ function commitMutationEffectsDeletions(
2593 );
2594 if (hasCaughtError()) {
2595 const error = clearCaughtError();
2596 - captureCommitPhaseError(childToDelete, error);
2596 + captureCommitPhaseError(childToDelete, childToDelete.return, error);
2597 }
2598 } else {
2599 try {
2600 commitDeletion(root, childToDelete, renderPriorityLevel);
2601 } catch (error) {
2602 - captureCommitPhaseError(childToDelete, error);
2602 + captureCommitPhaseError(childToDelete, childToDelete.return, error);
2603 }
2604 }
2605 }
@@ -2641,14 +2641,14 @@ function commitLayoutEffects(
2641 );
2642 if (hasCaughtError()) {
2643 const error = clearCaughtError();
2644 - captureCommitPhaseError(fiber, error);
2644 + captureCommitPhaseError(fiber, fiber.return, error);
2645 }
2646 resetCurrentDebugFiberInDEV();
2647 } else {
2648 try {
2649 commitLayoutEffectsImpl(fiber, root, committedLanes);
2650 } catch (error) {
2651 - captureCommitPhaseError(fiber, error);
2651 + captureCommitPhaseError(fiber, fiber.return, error);
2652 }
2653 }
2654 fiber = fiber.sibling;
@@ -2748,7 +2748,7 @@ function flushPassiveUnmountEffects(firstChild: Fiber): void {
2748 if (deletions !== null) {
2749 for (let i = 0; i < deletions.length; i++) {
2750 const fiberToDelete = deletions[i];
2751 - flushPassiveUnmountEffectsInsideOfDeletedTree(fiberToDelete);
2751 + flushPassiveUnmountEffectsInsideOfDeletedTree(fiberToDelete, fiber);
2752
2753 // Now that passive effects have been processed, it's safe to detach lingering pointers.
2754 detachFiberAfterEffects(fiberToDelete);
@@ -2780,6 +2780,7 @@ function flushPassiveUnmountEffects(firstChild: Fiber): void {
2780
2781 function flushPassiveUnmountEffectsInsideOfDeletedTree(
2782 fiberToDelete: Fiber,
2783 + nearestMountedAncestor: Fiber,
2784 ): void {
2785 if ((fiberToDelete.subtreeTag & PassiveStaticSubtreeTag) !== NoSubtreeTag) {
2786 // If any children have passive effects then traverse the subtree.
@@ -2788,14 +2789,17 @@ function flushPassiveUnmountEffectsInsideOfDeletedTree(
2789 // since that would not cover passive effects in siblings.
2790 let child = fiberToDelete.child;
2791 while (child !== null) {
2791 - flushPassiveUnmountEffectsInsideOfDeletedTree(child);
2792 + flushPassiveUnmountEffectsInsideOfDeletedTree(
2793 + child,
2794 + nearestMountedAncestor,
2795 + );
2796 child = child.sibling;
2797 }
2798 }
2799
2800 if ((fiberToDelete.effectTag & PassiveStatic) !== NoEffect) {
2801 setCurrentDebugFiberInDEV(fiberToDelete);
2798 - commitPassiveUnmount(fiberToDelete);
2802 + commitPassiveUnmount(fiberToDelete, nearestMountedAncestor);
2803 resetCurrentDebugFiberInDEV();
2804 }
2805 }
@@ -2922,7 +2926,11 @@ function captureCommitPhaseErrorOnRoot(
2926 }
2927 }
2928
2925 -export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2929 +export function captureCommitPhaseError(
2930 + sourceFiber: Fiber,
2931 + nearestMountedAncestor: Fiber | null,
2932 + error: mixed,
2933 +) {
2934 if (sourceFiber.tag === HostRoot) {
2935 // Error was thrown at the root. There is no parent, so the root
2936 // itself should capture it.
@@ -2930,7 +2938,7 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2938 return;
2939 }
2940
2933 - let fiber = sourceFiber.return;
2941 + let fiber = nearestMountedAncestor;
2942 while (fiber !== null) {
2943 if (fiber.tag === HostRoot) {
2944 captureCommitPhaseErrorOnRoot(fiber, sourceFiber, error);
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+18
@@ -2850,6 +2850,24 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
2850 markRootUpdated(root, SyncLane, eventTime);
2851 ensureRootIsScheduled(root, eventTime);
2852 schedulePendingInteractions(root, SyncLane);
2853 + } else {
2854 + // This component has already been unmounted.
2855 + // We can't schedule any follow up work for the root because the fiber is already unmounted,
2856 + // but we can still call the log-only boundary so the error isn't swallowed.
2857 + //
2858 + // TODO This is only a temporary bandaid for the old reconciler fork.
2859 + // We can delete this special case once the new fork is merged.
2860 + if (
2861 + typeof instance.componentDidCatch === 'function' &&
2862 + !isAlreadyFailedLegacyErrorBoundary(instance)
2863 + ) {
2864 + try {
2865 + instance.componentDidCatch(error, errorInfo);
2866 + } catch (errorToIgnore) {
2867 + // TODO Ignore this error? Rethrow it?
2868 + // This is kind of an edge case.
2869 + }
2870 + }
2871 }
2872 return;
2873 }
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+288 -6
@@ -2320,6 +2320,7 @@ describe('ReactHooksWithNoopRenderer', () => {
2320 describe('errors thrown in passive destroy function within unmounted trees', () => {
2321 let BrokenUseEffectCleanup;
2322 let ErrorBoundary;
2323 + let DerivedStateOnlyErrorBoundary;
2324 let LogOnlyErrorBoundary;
2325
2326 beforeEach(() => {
@@ -2351,10 +2352,32 @@ describe('ReactHooksWithNoopRenderer', () => {
2352 render() {
2353 if (this.state.error) {
2354 Scheduler.unstable_yieldValue('ErrorBoundary render error');
2354 - return 'ErrorBoundary fallback';
2355 + return <span prop="ErrorBoundary fallback" />;
2356 }
2357 Scheduler.unstable_yieldValue('ErrorBoundary render success');
2357 - return this.props.children;
2358 + return this.props.children || null;
2359 + }
2360 + };
2361 +
2362 + DerivedStateOnlyErrorBoundary = class extends React.Component {
2363 + state = {error: null};
2364 + static getDerivedStateFromError(error) {
2365 + Scheduler.unstable_yieldValue(
2366 + `DerivedStateOnlyErrorBoundary static getDerivedStateFromError`,
2367 + );
2368 + return {error};
2369 + }
2370 + render() {
2371 + if (this.state.error) {
2372 + Scheduler.unstable_yieldValue(
2373 + 'DerivedStateOnlyErrorBoundary render error',
2374 + );
2375 + return <span prop="DerivedStateOnlyErrorBoundary fallback" />;
2376 + }
2377 + Scheduler.unstable_yieldValue(
2378 + 'DerivedStateOnlyErrorBoundary render success',
2379 + );
2380 + return this.props.children || null;
2381 }
2382 };
2383
@@ -2366,12 +2389,13 @@ describe('ReactHooksWithNoopRenderer', () => {
2389 }
2390 render() {
2391 Scheduler.unstable_yieldValue(`LogOnlyErrorBoundary render`);
2369 - return this.props.children;
2392 + return this.props.children || null;
2393 }
2394 };
2395 });
2396
2374 - it('should not error if the nearest unmounted boundary is log-only', () => {
2397 + // @gate old
2398 + it('should call componentDidCatch() for the nearest unmounted log-only boundary', () => {
2399 function Conditional({showChildren}) {
2400 if (showChildren) {
2401 return (
@@ -2411,10 +2435,268 @@ describe('ReactHooksWithNoopRenderer', () => {
2435
2436 expect(Scheduler).toHaveYielded([
2437 'BrokenUseEffectCleanup useEffect destroy',
2414 - // This should call componentDidCatch too, but we'll address that in a follow up.
2415 - // 'LogOnlyErrorBoundary componentDidCatch',
2438 + 'LogOnlyErrorBoundary componentDidCatch',
2439 ]);
2440 });
2441 +
2442 + // @gate old
2443 + it('should call componentDidCatch() for the nearest unmounted logging-capable boundary', () => {
2444 + function Conditional({showChildren}) {
2445 + if (showChildren) {
2446 + return (
2447 + <ErrorBoundary>
2448 + <BrokenUseEffectCleanup />
2449 + </ErrorBoundary>
2450 + );
2451 + } else {
2452 + return null;
2453 + }
2454 + }
2455 +
2456 + act(() => {
2457 + ReactNoop.render(
2458 + <ErrorBoundary>
2459 + <Conditional showChildren={true} />
2460 + </ErrorBoundary>,
2461 + );
2462 + });
2463 +
2464 + expect(Scheduler).toHaveYielded([
2465 + 'ErrorBoundary render success',
2466 + 'ErrorBoundary render success',
2467 + 'BrokenUseEffectCleanup useEffect',
2468 + ]);
2469 +
2470 + act(() => {
2471 + ReactNoop.render(
2472 + <ErrorBoundary>
2473 + <Conditional showChildren={false} />
2474 + </ErrorBoundary>,
2475 + );
2476 + expect(Scheduler).toFlushAndYieldThrough([
2477 + 'ErrorBoundary render success',
2478 + ]);
2479 + });
2480 +
2481 + expect(Scheduler).toHaveYielded([
2482 + 'BrokenUseEffectCleanup useEffect destroy',
2483 + 'ErrorBoundary componentDidCatch',
2484 + ]);
2485 + });
2486 +
2487 + // @gate old
2488 + it('should not call getDerivedStateFromError for unmounted error boundaries', () => {
2489 + function Conditional({showChildren}) {
2490 + if (showChildren) {
2491 + return (
2492 + <ErrorBoundary>
2493 + <BrokenUseEffectCleanup />
2494 + </ErrorBoundary>
2495 + );
2496 + } else {
2497 + return null;
2498 + }
2499 + }
2500 +
2501 + act(() => {
2502 + ReactNoop.render(<Conditional showChildren={true} />);
2503 + });
2504 +
2505 + expect(Scheduler).toHaveYielded([
2506 + 'ErrorBoundary render success',
2507 + 'BrokenUseEffectCleanup useEffect',
2508 + ]);
2509 +
2510 + act(() => {
2511 + ReactNoop.render(<Conditional showChildren={false} />);
2512 + });
2513 +
2514 + expect(Scheduler).toHaveYielded([
2515 + 'BrokenUseEffectCleanup useEffect destroy',
2516 + 'ErrorBoundary componentDidCatch',
2517 + ]);
2518 + });
2519 +
2520 + // @gate old
2521 + it('should not throw if there are no unmounted logging-capable boundaries to call', () => {
2522 + function Conditional({showChildren}) {
2523 + if (showChildren) {
2524 + return (
2525 + <DerivedStateOnlyErrorBoundary>
2526 + <BrokenUseEffectCleanup />
2527 + </DerivedStateOnlyErrorBoundary>
2528 + );
2529 + } else {
2530 + return null;
2531 + }
2532 + }
2533 +
2534 + act(() => {
2535 + ReactNoop.render(<Conditional showChildren={true} />);
2536 + });
2537 +
2538 + expect(Scheduler).toHaveYielded([
2539 + 'DerivedStateOnlyErrorBoundary render success',
2540 + 'BrokenUseEffectCleanup useEffect',
2541 + ]);
2542 +
2543 + act(() => {
2544 + ReactNoop.render(<Conditional showChildren={false} />);
2545 + });
2546 +
2547 + expect(Scheduler).toHaveYielded([
2548 + 'BrokenUseEffectCleanup useEffect destroy',
2549 + ]);
2550 + });
2551 +
2552 + // @gate new
2553 + it('should use the nearest still-mounted boundary if there are no unmounted boundaries', () => {
2554 + act(() => {
2555 + ReactNoop.render(
2556 + <LogOnlyErrorBoundary>
2557 + <BrokenUseEffectCleanup />
2558 + </LogOnlyErrorBoundary>,
2559 + );
2560 + });
2561 +
2562 + expect(Scheduler).toHaveYielded([
2563 + 'LogOnlyErrorBoundary render',
2564 + 'BrokenUseEffectCleanup useEffect',
2565 + ]);
2566 +
2567 + act(() => {
2568 + ReactNoop.render(<LogOnlyErrorBoundary />);
2569 + });
2570 +
2571 + expect(Scheduler).toHaveYielded([
2572 + 'LogOnlyErrorBoundary render',
2573 + 'BrokenUseEffectCleanup useEffect destroy',
2574 + 'LogOnlyErrorBoundary componentDidCatch',
2575 + ]);
2576 + });
2577 +
2578 + // @gate new
2579 + it('should skip unmounted boundaries and use the nearest still-mounted boundary', () => {
2580 + function Conditional({showChildren}) {
2581 + if (showChildren) {
2582 + return (
2583 + <ErrorBoundary>
2584 + <BrokenUseEffectCleanup />
2585 + </ErrorBoundary>
2586 + );
2587 + } else {
2588 + return null;
2589 + }
2590 + }
2591 +
2592 + act(() => {
2593 + ReactNoop.render(
2594 + <LogOnlyErrorBoundary>
2595 + <Conditional showChildren={true} />
2596 + </LogOnlyErrorBoundary>,
2597 + );
2598 + });
2599 +
2600 + expect(Scheduler).toHaveYielded([
2601 + 'LogOnlyErrorBoundary render',
2602 + 'ErrorBoundary render success',
2603 + 'BrokenUseEffectCleanup useEffect',
2604 + ]);
2605 +
2606 + act(() => {
2607 + ReactNoop.render(
2608 + <LogOnlyErrorBoundary>
2609 + <Conditional showChildren={false} />
2610 + </LogOnlyErrorBoundary>,
2611 + );
2612 + });
2613 +
2614 + expect(Scheduler).toHaveYielded([
2615 + 'LogOnlyErrorBoundary render',
2616 + 'BrokenUseEffectCleanup useEffect destroy',
2617 + 'LogOnlyErrorBoundary componentDidCatch',
2618 + ]);
2619 + });
2620 +
2621 + // @gate new
2622 + it('should call getDerivedStateFromError in the nearest still-mounted boundary', () => {
2623 + function Conditional({showChildren}) {
2624 + if (showChildren) {
2625 + return <BrokenUseEffectCleanup />;
2626 + } else {
2627 + return null;
2628 + }
2629 + }
2630 +
2631 + act(() => {
2632 + ReactNoop.render(
2633 + <ErrorBoundary>
2634 + <Conditional showChildren={true} />
2635 + </ErrorBoundary>,
2636 + );
2637 + });
2638 +
2639 + expect(Scheduler).toHaveYielded([
2640 + 'ErrorBoundary render success',
2641 + 'BrokenUseEffectCleanup useEffect',
2642 + ]);
2643 +
2644 + act(() => {
2645 + ReactNoop.render(
2646 + <ErrorBoundary>
2647 + <Conditional showChildren={false} />
2648 + </ErrorBoundary>,
2649 + );
2650 + });
2651 +
2652 + expect(Scheduler).toHaveYielded([
2653 + 'ErrorBoundary render success',
2654 + 'BrokenUseEffectCleanup useEffect destroy',
2655 + 'ErrorBoundary static getDerivedStateFromError',
2656 + 'ErrorBoundary render error',
2657 + 'ErrorBoundary componentDidCatch',
2658 + ]);
2659 +
2660 + expect(ReactNoop.getChildren()).toEqual([
2661 + span('ErrorBoundary fallback'),
2662 + ]);
2663 + });
2664 +
2665 + // @gate new
2666 + it('should rethrow error if there are no still-mounted boundaries', () => {
2667 + function Conditional({showChildren}) {
2668 + if (showChildren) {
2669 + return (
2670 + <ErrorBoundary>
2671 + <BrokenUseEffectCleanup />
2672 + </ErrorBoundary>
2673 + );
2674 + } else {
2675 + return null;
2676 + }
2677 + }
2678 +
2679 + act(() => {
2680 + ReactNoop.render(<Conditional showChildren={true} />);
2681 + });
2682 +
2683 + expect(Scheduler).toHaveYielded([
2684 + 'ErrorBoundary render success',
2685 + 'BrokenUseEffectCleanup useEffect',
2686 + ]);
2687 +
2688 + expect(() => {
2689 + act(() => {
2690 + ReactNoop.render(<Conditional showChildren={false} />);
2691 + });
2692 + }).toThrow('Expected error');
2693 +
2694 + expect(Scheduler).toHaveYielded([
2695 + 'BrokenUseEffectCleanup useEffect destroy',
2696 + ]);
2697 +
2698 + expect(ReactNoop.getChildren()).toEqual([]);
2699 + });
2700 });
2701 });
2702