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(() => {
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
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 (
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