[Fresh] Track unrecoverable errors (#16183)
* [Fresh] Track unrecoverable errors * Only initial errors set the flag
Dan Abramov committed
Jul 23, 2019 at 16:11 UTC
2237efcef9a8e63c1b3f1bd61481cb215d20986a
2 files changed
+46
packages/react-refresh/src/ReactFreshRuntime.js
+18
@@ -65,6 +65,7 @@ let findHostInstancesForRefresh: null | FindHostInstancesForRefresh = null;
65
let mountedRoots: Set<FiberRoot> = new Set();
66
// If a root captures an error, we add its element to this Map so we can retry on edit.
67
let failedRoots: Map<FiberRoot, ReactNodeList> = new Map();
68
+let didSomeRootFailOnMount = false;
69
70
function computeFullKey(signature: Signature): string {
71
if (signature.fullKey !== null) {
@@ -459,6 +460,19 @@ export function injectIntoGlobalHook(globalObject: any): void {
460
// Remember what was rendered so we can restore it.
461
failedRoots.set(root, alternate.memoizedState.element);
462
}
463
+ } else if (!wasMounted && !isMounted) {
464
+ if (didError && !failedRoots.has(root)) {
465
+ // The root had an error during the initial mount.
466
+ // We can't read its last element from the memoized state
467
+ // because there was no previously committed alternate.
468
+ // Ideally, it would be nice if we had a way to extract
469
+ // the last attempted rendered element, but accessing the update queue
470
+ // would tie this package too closely to the reconciler version.
471
+ // So instead, we just set a flag.
472
+ // TODO: Maybe we could fix this as the same time as when we fix
473
+ // DevTools to not depend on `alternate.memoizedState.element`.
474
+ didSomeRootFailOnMount = true;
475
+ }
476
}
477
} else {
478
// Mount a new root.
@@ -474,6 +488,10 @@ export function injectIntoGlobalHook(globalObject: any): void {
488
}
489
}
490
491
+export function hasUnrecoverableErrors() {
492
+ return didSomeRootFailOnMount;
493
+}
494
+
495
// Exposed for testing.
496
export function _getMountedRootCount() {
497
if (__DEV__) {
packages/react-refresh/src/__tests__/ReactFresh-test.js
+28
@@ -2712,8 +2712,28 @@ describe('ReactFresh', () => {
2712
}
2713
});
2714
2715
+ // TODO: we can make this recoverable in the future
2716
+ // if we add a way to track the last attempted element.
2717
+ it('records an unrecoverable error if a root fails on mount', () => {
2718
+ if (__DEV__) {
2719
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2720
+ expect(() => {
2721
+ render(() => {
2722
+ function Hello() {
2723
+ throw new Error('No');
2724
+ }
2725
+ $RefreshReg$(Hello, 'Hello');
2726
+
2727
+ return Hello;
2728
+ });
2729
+ }).toThrow('No');
2730
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(true);
2731
+ }
2732
+ });
2733
+
2734
it('remounts a failed root on update', () => {
2735
if (__DEV__) {
2736
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2737
render(() => {
2738
function Hello() {
2739
return <h1>Hi</h1>;
@@ -2723,6 +2743,7 @@ describe('ReactFresh', () => {
2743
return Hello;
2744
});
2745
expect(container.innerHTML).toBe('<h1>Hi</h1>');
2746
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2747
2748
// Perform a hot update that fails.
2749
// This removes the root.
@@ -2735,6 +2756,7 @@ describe('ReactFresh', () => {
2756
});
2757
}).toThrow('No');
2758
expect(container.innerHTML).toBe('');
2759
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2760
2761
// A bad retry
2762
expect(() => {
@@ -2746,6 +2768,7 @@ describe('ReactFresh', () => {
2768
});
2769
}).toThrow('Not yet');
2770
expect(container.innerHTML).toBe('');
2771
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2772
2773
// Perform a hot update that fixes the error.
2774
patch(() => {
@@ -2756,6 +2779,7 @@ describe('ReactFresh', () => {
2779
});
2780
// This should remount the root.
2781
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
2782
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2783
2784
// Verify next hot reload doesn't remount anything.
2785
let helloNode = container.firstChild;
@@ -2767,6 +2791,7 @@ describe('ReactFresh', () => {
2791
});
2792
expect(container.firstChild).toBe(helloNode);
2793
expect(helloNode.textContent).toBe('Nice.');
2794
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2795
2796
// Break again.
2797
expect(() => {
@@ -2778,6 +2803,7 @@ describe('ReactFresh', () => {
2803
});
2804
}).toThrow('Oops');
2805
expect(container.innerHTML).toBe('');
2806
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2807
2808
// Perform a hot update that fixes the error.
2809
patch(() => {
@@ -2788,6 +2814,7 @@ describe('ReactFresh', () => {
2814
});
2815
// This should remount the root.
2816
expect(container.innerHTML).toBe('<h1>At last.</h1>');
2817
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2818
2819
// Check we don't attempt to reverse an intentional unmount.
2820
ReactDOM.unmountComponentAtNode(container);
@@ -2799,6 +2826,7 @@ describe('ReactFresh', () => {
2826
$RefreshReg$(Hello, 'Hello');
2827
});
2828
expect(container.innerHTML).toBe('');
2829
+ expect(ReactFreshRuntime.hasUnrecoverableErrors()).toBe(false);
2830
}
2831
});
2832