From 0f8775a5ad8d6455d3f1401f6e5a3d38ed785716 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 22 Apr 2022 14:40:59 -0700 Subject: [PATCH 01/13] Avoid accumulating hydration mismatch errors after the first hydration error If there is a suspended component or an error during hydration there will almost certainly be many additional hydration mismatch errors because the hydration target does not pair up the server rendered html with an expected slot on the client. To avoid spamming users with warnings there was already logic in place to suppress console warnings if such an occurrence happens. This commit takes another approach to avoid queueing thrown errors. when suspending this isn't that big of an issue becuase queued errors are discarded becasue the suspense boundary does not complete. When erroring within a resolved suspense boundary however the root completes and all queued errors are upgraded to recoverable errors and in many cases wihll flood the console. What is worse is the console warnings which offer much more specific guidance on what went wrong (in dev) are suppressed so the user is left with very little actionable information on which to go on and the volume of mismatch errors may distract from identifying the root cause error The hueristic is as follows 1. always queue the first error during hydration 2. always queue non hydration mismatch errors 2. discard hydration mismatch errors before queueing If there is an already queued error or any type --- .../src/__tests__/ReactDOMFizzServer-test.js | 4 -- .../src/ReactFiberHydrationContext.new.js | 58 +++++++++++++------ .../src/ReactFiberHydrationContext.old.js | 58 +++++++++++++------ 3 files changed, 80 insertions(+), 40 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index bf5388597571..1ce7c7bc75a3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2976,8 +2976,6 @@ describe('ReactDOMFizzServer', () => { }, }); expect(Scheduler).toFlushAndYield([ - 'Logged recoverable error: Text content does not match server-rendered HTML.', - 'Logged recoverable error: Text content does not match server-rendered HTML.', 'Logged recoverable error: Text content does not match server-rendered HTML.', 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', ]); @@ -3069,8 +3067,6 @@ describe('ReactDOMFizzServer', () => { }); expect(Scheduler).toFlushAndYield([ 'Logged recoverable error: uh oh', - 'Logged recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', - 'Logged recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', ]); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index e0e592d32790..d0471f2fda34 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -386,10 +386,12 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { } function throwOnHydrationMismatch(fiber: Fiber) { - throw new Error( + const error = new Error( 'Hydration failed because the initial UI does not match what was ' + 'rendered on the server.', ); + (error: any)._hydrationMismatch = true; + throw error; } function tryToClaimNextHydratableInstance(fiber: Fiber): void { @@ -448,23 +450,32 @@ function prepareToHydrateHostInstance( const instance: Instance = fiber.stateNode; const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV; - const updatePayload = hydrateInstance( - instance, - fiber.type, - fiber.memoizedProps, - rootContainerInstance, - hostContext, - fiber, - shouldWarnIfMismatchDev, - ); - // TODO: Type this specific to this type of component. - fiber.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload !== null) { - return true; + try { + const updatePayload = hydrateInstance( + instance, + fiber.type, + fiber.memoizedProps, + rootContainerInstance, + hostContext, + fiber, + shouldWarnIfMismatchDev, + ); + + // TODO: Type this specific to this type of component. + fiber.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. + if (updatePayload !== null) { + return true; + } + return false; + } catch (error) { + // We use an expando to decorate errors arising from hydration matching + // so we can optionally discard them if a more fundamental preceding + // hydration error has occurred. + error._hydrationMismatch = true; + throw error; } - return false; } function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { @@ -675,9 +686,20 @@ function getIsHydrating(): boolean { export function queueHydrationError(error: mixed): void { if (hydrationErrors === null) { + // We always queue the first hydration error hydrationErrors = [error]; } else { - hydrationErrors.push(error); + if ((error: any)._hydrationMismatch !== true) { + // If there is at least one hydrationError we suppress additional + // hydrationMismatch errors on the logic that the earlier error + // will lead to a cascade of mismatch errors. + // If the boundary is suspending then there will be another hydration + // opportunity in a future render. + // If the boundary is not suspending then the earlier errors are + // the proximal cause and the mismatch errors are almost certainly + // a distraction + hydrationErrors.push(error); + } } } diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index a4fd5c93e22e..baad232710b4 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -386,10 +386,12 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { } function throwOnHydrationMismatch(fiber: Fiber) { - throw new Error( + const error = new Error( 'Hydration failed because the initial UI does not match what was ' + 'rendered on the server.', ); + (error: any)._hydrationMismatch = true; + throw error; } function tryToClaimNextHydratableInstance(fiber: Fiber): void { @@ -448,23 +450,32 @@ function prepareToHydrateHostInstance( const instance: Instance = fiber.stateNode; const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV; - const updatePayload = hydrateInstance( - instance, - fiber.type, - fiber.memoizedProps, - rootContainerInstance, - hostContext, - fiber, - shouldWarnIfMismatchDev, - ); - // TODO: Type this specific to this type of component. - fiber.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload !== null) { - return true; + try { + const updatePayload = hydrateInstance( + instance, + fiber.type, + fiber.memoizedProps, + rootContainerInstance, + hostContext, + fiber, + shouldWarnIfMismatchDev, + ); + + // TODO: Type this specific to this type of component. + fiber.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. + if (updatePayload !== null) { + return true; + } + return false; + } catch (error) { + // We use an expando to decorate errors arising from hydration matching + // so we can optionally discard them if a more fundamental preceding + // hydration error has occurred. + error._hydrationMismatch = true; + throw error; } - return false; } function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { @@ -675,9 +686,20 @@ function getIsHydrating(): boolean { export function queueHydrationError(error: mixed): void { if (hydrationErrors === null) { + // We always queue the first hydration error hydrationErrors = [error]; } else { - hydrationErrors.push(error); + if ((error: any)._hydrationMismatch !== true) { + // If there is at least one hydrationError we suppress additional + // hydrationMismatch errors on the logic that the earlier error + // will lead to a cascade of mismatch errors. + // If the boundary is suspending then there will be another hydration + // opportunity in a future render. + // If the boundary is not suspending then the earlier errors are + // the proximal cause and the mismatch errors are almost certainly + // a distraction + hydrationErrors.push(error); + } } } From 72fa9bb4e9a19da6e8bfdfcf6bea74c7e2f21ce2 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 13:11:42 -0700 Subject: [PATCH 02/13] Reduce uncaught errors caused by IGC and refactor mismatch errors to not use error properties --- ...DOMServerPartialHydration-test.internal.js | 4 +- .../src/ReactFiberHydrationContext.new.js | 37 +++++++++++-------- .../src/ReactFiberWorkLoop.new.js | 11 ++++-- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index fe9d8dd453cd..dec9656c3383 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -350,9 +350,7 @@ describe('ReactDOMServerPartialHydration', () => { ); if (__DEV__) { - const secondToLastCall = - mockError.mock.calls[mockError.mock.calls.length - 2]; - expect(secondToLastCall).toEqual([ + expect(mockError.mock.calls[0]).toEqual([ 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s', 'article', 'section', diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index d0471f2fda34..a3b173b5870b 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -88,6 +88,14 @@ let didSuspendOrErrorDEV: boolean = false; // Hydration errors that were thrown inside this boundary let hydrationErrors: Array | null = null; +// When true we expect the next queued hydration error to be a mismatch error. +// These errors are handled differently than other errors that occurr during hydration +let didThrowNotYetQueuedHydrationMismatchError = false; + +export function hydrationDidSuspendOrErrorDEV() { + return didSuspendOrErrorDEV; +} + function warnIfHydrating() { if (__DEV__) { if (isHydrating) { @@ -386,12 +394,11 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { } function throwOnHydrationMismatch(fiber: Fiber) { - const error = new Error( + didThrowNotYetQueuedHydrationMismatchError = true; + throw new Error( 'Hydration failed because the initial UI does not match what was ' + 'rendered on the server.', ); - (error: any)._hydrationMismatch = true; - throw error; } function tryToClaimNextHydratableInstance(fiber: Fiber): void { @@ -470,10 +477,7 @@ function prepareToHydrateHostInstance( } return false; } catch (error) { - // We use an expando to decorate errors arising from hydration matching - // so we can optionally discard them if a more fundamental preceding - // hydration error has occurred. - error._hydrationMismatch = true; + didThrowNotYetQueuedHydrationMismatchError = true; throw error; } } @@ -689,18 +693,19 @@ export function queueHydrationError(error: mixed): void { // We always queue the first hydration error hydrationErrors = [error]; } else { - if ((error: any)._hydrationMismatch !== true) { - // If there is at least one hydrationError we suppress additional - // hydrationMismatch errors on the logic that the earlier error - // will lead to a cascade of mismatch errors. - // If the boundary is suspending then there will be another hydration - // opportunity in a future render. - // If the boundary is not suspending then the earlier errors are - // the proximal cause and the mismatch errors are almost certainly - // a distraction + // didThrowNotYetQueuedHydrationMismatchError will be true if we just threw + // a hydration mismatch error. In those cases we do not want to queue the + // error because there is a more useful error related to hydration that + // was already queued. + if (!didThrowNotYetQueuedHydrationMismatchError) { + // this error came from somewhere other than a hydration mismatch so we + // queue it even though other errors have also been queued. The user + // should see these errors if a recovery is made hydrationErrors.push(error); } } + // Now that we have handled the queueing request we reset this + didThrowNotYetQueuedHydrationMismatchError = false; } export { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index aa20335cbec9..d6d61a9d663d 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -88,6 +88,7 @@ import { assignFiberPropertiesInDEV, } from './ReactFiber.new'; import {isRootDehydrated} from './ReactFiberShellHydration'; +import {hydrationDidSuspendOrErrorDEV} from './ReactFiberHydrationContext.new'; import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, @@ -3001,11 +3002,13 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { return originalBeginWork(current, unitOfWork, lanes); } catch (originalError) { if ( - originalError !== null && - typeof originalError === 'object' && - typeof originalError.then === 'function' + hydrationDidSuspendOrErrorDEV() || + (originalError !== null && + typeof originalError === 'object' && + typeof originalError.then === 'function') ) { - // Don't replay promises. Treat everything else like an error. + // Don't replay promises. + // Don't replay when hydration has suspended or errored already throw originalError; } From 136a2acade02a7f2569d9d15f3afa9894006b11b Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 13:16:53 -0700 Subject: [PATCH 03/13] add test for matching tree hydration error warnings --- .../src/__tests__/ReactDOMFizzServer-test.js | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 1ce7c7bc75a3..e5d1b0b0a154 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2842,8 +2842,84 @@ describe('ReactDOMFizzServer', () => { }); }); + // @gate experimental + it('#24384: Suspending should halt hydration warnings and not emit any if hydration completes successfully after unsuspending', async () => { + const makeApp = () => { + let resolve, resolved; + const promise = new Promise(r => { + resolve = () => { + resolved = true; + return r(); + }; + }); + function ComponentThatSuspends() { + if (!resolved) { + throw promise; + } + return

A

; + } + + const App = () => { + return ( +
+ Loading...}> + +

world

+
+
+ ); + }; + + return [App, resolve]; + }; + + const [ServerApp, serverResolve] = makeApp(); + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + await act(() => { + serverResolve(); + }); + + expect(getVisibleChildren(container)).toEqual( +
+

A

+

world

+
, + ); + + const [ClientApp, clientResolve] = makeApp(); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Logged recoverable error: ' + error.message, + ); + }, + }); + Scheduler.unstable_flushAll(); + + expect(getVisibleChildren(container)).toEqual( +
+

A

+

world

+
, + ); + + // Now that the boundary resolves to it's children the hydration completes and discovers that there is a mismatch requiring + // client-side rendering. + await clientResolve(); + expect(Scheduler).toFlushWithoutYielding(); + expect(getVisibleChildren(container)).toEqual( +
+

A

+

world

+
, + ); + }); + // @gate experimental && enableClientRenderFallbackOnTextMismatch - it('#24384: Suspending should halt hydration warnings while still allowing siblings to warm up', async () => { + it('#24384: Suspending should halt hydration warnings but still emit hydration warnings after unsuspending if mismatches are genuine', async () => { const makeApp = () => { let resolve, resolved; const promise = new Promise(r => { From 4783e68096f1b045725994792b153da152eb05ba Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 15:38:41 -0700 Subject: [PATCH 04/13] add tests to assert that invokeGuardedCallback is only called when desired now --- .../src/__tests__/ReactDOMFizzServer-test.js | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index e5d1b0b0a154..dad69290830b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1207,6 +1207,9 @@ describe('ReactDOMFizzServer', () => { }); function normalizeCodeLocInfo(str) { + if (typeof str === 'object') { + return; + } return ( str && str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) { @@ -3156,4 +3159,217 @@ describe('ReactDOMFizzServer', () => { expect(Scheduler).toFlushAndYield([]); }); + + // @gate __DEV__ + it('does not invokeGuardedCallback for errors after the first hydration error', async () => { + // We can't use the toErrorDev helper here because this is async. + const originalConsoleError = console.error; + const mockError = jest.fn(); + console.error = (...args) => { + if (args.length > 1) { + if (typeof args[1] === 'object') { + mockError(args[0].split('\n')[0]); + return; + } + } + mockError(...args.map(normalizeCodeLocInfo)); + }; + let isClient = false; + let shouldThrow = true; + + function ThrowUntilOnClient({children, message}) { + if (isClient && shouldThrow) { + throw new Error(message); + } + return children; + } + + function StopThrowingOnClient() { + if (isClient) { + shouldThrow = false; + } + return null; + } + + const App = () => { + return ( +
+ Loading...}> + +

one

+
+ +

two

+
+ +

three

+
+ +
+
+ ); + }; + + try { + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + + isClient = true; + + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Logged recoverable error: ' + error.message, + ); + }, + }); + expect(Scheduler).toFlushAndYield([ + 'Logged recoverable error: first error', + 'Logged recoverable error: second error', + 'Logged recoverable error: third error', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + ]); + + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + + expect(Scheduler).toFlushAndYield([]); + + // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use) + // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document + expect(mockError.mock.calls).toEqual([ + // we only get one because we suppress invokeGuardedCallback after the first one when hydrating in a + // suspense boundary + ['Error: Uncaught [Error: first error]'], + ]); + } finally { + console.error = originalConsoleError; + } + }); + // @gate __DEV__ + it('does not invokeGuardedCallback for errors after a preceding fiber suspends', async () => { + // We can't use the toErrorDev helper here because this is async. + const originalConsoleError = console.error; + const mockError = jest.fn(); + console.error = (...args) => { + if (args.length > 1) { + if (typeof args[1] === 'object') { + mockError(args[0].split('\n')[0]); + return; + } + } + mockError(...args.map(normalizeCodeLocInfo)); + }; + let isClient = false; + let shouldThrow = true; + let promise = null; + let unsuspend = null; + let isResolved = false; + + function ComponentThatSuspendsOnClient() { + if (isClient && !isResolved) { + if (promise === null) { + promise = new Promise(resolve => { + unsuspend = () => { + isResolved = true; + resolve(); + }; + }); + } + throw promise; + } + return null; + } + + function ThrowUntilOnClient({children, message}) { + if (isClient && shouldThrow) { + throw new Error(message); + } + return children; + } + + function StopThrowingOnClient() { + if (isClient) { + shouldThrow = false; + } + return null; + } + + const App = () => { + return ( +
+ Loading...}> + + +

one

+
+ +

two

+
+ +

three

+
+ +
+
+ ); + }; + + try { + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + + isClient = true; + + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Logged recoverable error: ' + error.message, + ); + }, + }); + expect(Scheduler).toFlushAndYield([]); + + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + await unsuspend(); + expect(Scheduler).toFlushAndYield([]); + + // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use) + // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document + expect(mockError.mock.calls).toEqual([]); + } finally { + console.error = originalConsoleError; + } + }); }); From 3773d00510b93e5a8408fb19b224dad1001b3a0f Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 15:47:20 -0700 Subject: [PATCH 05/13] catch up forks --- .../src/ReactFiberHydrationContext.old.js | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index baad232710b4..91a299058791 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -88,6 +88,14 @@ let didSuspendOrErrorDEV: boolean = false; // Hydration errors that were thrown inside this boundary let hydrationErrors: Array | null = null; +// When true we expect the next queued hydration error to be a mismatch error. +// These errors are handled differently than other errors that occurr during hydration +let didThrowNotYetQueuedHydrationMismatchError = false; + +export function hydrationDidSuspendOrErrorDEV() { + return didSuspendOrErrorDEV; +} + function warnIfHydrating() { if (__DEV__) { if (isHydrating) { @@ -386,12 +394,11 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { } function throwOnHydrationMismatch(fiber: Fiber) { - const error = new Error( + didThrowNotYetQueuedHydrationMismatchError = true; + throw new Error( 'Hydration failed because the initial UI does not match what was ' + 'rendered on the server.', ); - (error: any)._hydrationMismatch = true; - throw error; } function tryToClaimNextHydratableInstance(fiber: Fiber): void { @@ -470,10 +477,7 @@ function prepareToHydrateHostInstance( } return false; } catch (error) { - // We use an expando to decorate errors arising from hydration matching - // so we can optionally discard them if a more fundamental preceding - // hydration error has occurred. - error._hydrationMismatch = true; + didThrowNotYetQueuedHydrationMismatchError = true; throw error; } } @@ -689,18 +693,19 @@ export function queueHydrationError(error: mixed): void { // We always queue the first hydration error hydrationErrors = [error]; } else { - if ((error: any)._hydrationMismatch !== true) { - // If there is at least one hydrationError we suppress additional - // hydrationMismatch errors on the logic that the earlier error - // will lead to a cascade of mismatch errors. - // If the boundary is suspending then there will be another hydration - // opportunity in a future render. - // If the boundary is not suspending then the earlier errors are - // the proximal cause and the mismatch errors are almost certainly - // a distraction + // didThrowNotYetQueuedHydrationMismatchError will be true if we just threw + // a hydration mismatch error. In those cases we do not want to queue the + // error because there is a more useful error related to hydration that + // was already queued. + if (!didThrowNotYetQueuedHydrationMismatchError) { + // this error came from somewhere other than a hydration mismatch so we + // queue it even though other errors have also been queued. The user + // should see these errors if a recovery is made hydrationErrors.push(error); } } + // Now that we have handled the queueing request we reset this + didThrowNotYetQueuedHydrationMismatchError = false; } export { From f15c5f2c711c41c390476b048274a26772fd9ce9 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 15:48:13 -0700 Subject: [PATCH 06/13] merge workloop fork --- .../react-reconciler/src/ReactFiberWorkLoop.old.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index e2c3a76c7fc2..d4695d2f28b9 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -88,6 +88,7 @@ import { assignFiberPropertiesInDEV, } from './ReactFiber.old'; import {isRootDehydrated} from './ReactFiberShellHydration'; +import {hydrationDidSuspendOrErrorDEV} from './ReactFiberHydrationContext.new'; import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, @@ -3001,11 +3002,13 @@ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { return originalBeginWork(current, unitOfWork, lanes); } catch (originalError) { if ( - originalError !== null && - typeof originalError === 'object' && - typeof originalError.then === 'function' + hydrationDidSuspendOrErrorDEV() || + (originalError !== null && + typeof originalError === 'object' && + typeof originalError.then === 'function') ) { - // Don't replay promises. Treat everything else like an error. + // Don't replay promises. + // Don't replay when hydration has suspended or errored already throw originalError; } From 964831490121dc969b451255f825bbd201162cf4 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 15:50:17 -0700 Subject: [PATCH 07/13] fix import --- packages/react-reconciler/src/ReactFiberWorkLoop.old.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index d4695d2f28b9..2978f08ab2c2 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -88,7 +88,7 @@ import { assignFiberPropertiesInDEV, } from './ReactFiber.old'; import {isRootDehydrated} from './ReactFiberShellHydration'; -import {hydrationDidSuspendOrErrorDEV} from './ReactFiberHydrationContext.new'; +import {hydrationDidSuspendOrErrorDEV} from './ReactFiberHydrationContext.old'; import {NoMode, ProfileMode, ConcurrentMode} from './ReactTypeOfMode'; import { HostRoot, From f531201f89aabb254467f28806dd834bc697491c Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 15:56:23 -0700 Subject: [PATCH 08/13] fix test gates --- packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index dad69290830b..f043494a70e6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3261,7 +3261,9 @@ describe('ReactDOMFizzServer', () => { console.error = originalConsoleError; } }); - // @gate __DEV__ + + // This test is not gated to __DEV__ because in prod there is no iGC call for these errors and so it passes + // there too it('does not invokeGuardedCallback for errors after a preceding fiber suspends', async () => { // We can't use the toErrorDev helper here because this is async. const originalConsoleError = console.error; From a84b1ad90b384cf1712c62c0a1611b847a217c94 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 16:02:28 -0700 Subject: [PATCH 09/13] more gates --- packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index f043494a70e6..85ffe1a1ebd8 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3160,7 +3160,7 @@ describe('ReactDOMFizzServer', () => { expect(Scheduler).toFlushAndYield([]); }); - // @gate __DEV__ + // @gate experimental && __DEV__ it('does not invokeGuardedCallback for errors after the first hydration error', async () => { // We can't use the toErrorDev helper here because this is async. const originalConsoleError = console.error; @@ -3262,8 +3262,7 @@ describe('ReactDOMFizzServer', () => { } }); - // This test is not gated to __DEV__ because in prod there is no iGC call for these errors and so it passes - // there too + // @gate experimental it('does not invokeGuardedCallback for errors after a preceding fiber suspends', async () => { // We can't use the toErrorDev helper here because this is async. const originalConsoleError = console.error; From e74488b5dd8a275d452baefa7495ea2f6907feaa Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 28 Apr 2022 16:34:20 -0700 Subject: [PATCH 10/13] reset throw state on hydration entry and reset --- packages/react-reconciler/src/ReactFiberHydrationContext.new.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index a3b173b5870b..bf70f3f6c712 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -125,6 +125,7 @@ function enterHydrationState(fiber: Fiber): boolean { isHydrating = true; hydrationErrors = null; didSuspendOrErrorDEV = false; + didThrowNotYetQueuedHydrationMismatchError = false; return true; } @@ -143,6 +144,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance( isHydrating = true; hydrationErrors = null; didSuspendOrErrorDEV = false; + didThrowNotYetQueuedHydrationMismatchError = false; if (treeContext !== null) { restoreSuspendedTreeContext(fiber, treeContext); } From 87d8ef5e735489a6e545d0b9bd4dd386e1bf4a55 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 29 Apr 2022 14:26:37 -0700 Subject: [PATCH 11/13] only queue first thrown hydration error --- .../src/__tests__/ReactDOMFizzServer-test.js | 2 - .../src/ReactFiberHydrationContext.new.js | 68 +++++++------------ .../src/ReactFiberHydrationContext.old.js | 66 +++++++----------- .../src/ReactFiberThrow.new.js | 4 +- .../src/ReactFiberThrow.old.js | 7 +- 5 files changed, 60 insertions(+), 87 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 85ffe1a1ebd8..3cbd652e342a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3235,8 +3235,6 @@ describe('ReactDOMFizzServer', () => { }); expect(Scheduler).toFlushAndYield([ 'Logged recoverable error: first error', - 'Logged recoverable error: second error', - 'Logged recoverable error: third error', 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', ]); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js index bf70f3f6c712..81a747cdc22e 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js @@ -88,10 +88,6 @@ let didSuspendOrErrorDEV: boolean = false; // Hydration errors that were thrown inside this boundary let hydrationErrors: Array | null = null; -// When true we expect the next queued hydration error to be a mismatch error. -// These errors are handled differently than other errors that occurr during hydration -let didThrowNotYetQueuedHydrationMismatchError = false; - export function hydrationDidSuspendOrErrorDEV() { return didSuspendOrErrorDEV; } @@ -125,7 +121,6 @@ function enterHydrationState(fiber: Fiber): boolean { isHydrating = true; hydrationErrors = null; didSuspendOrErrorDEV = false; - didThrowNotYetQueuedHydrationMismatchError = false; return true; } @@ -144,7 +139,6 @@ function reenterHydrationStateFromDehydratedSuspenseInstance( isHydrating = true; hydrationErrors = null; didSuspendOrErrorDEV = false; - didThrowNotYetQueuedHydrationMismatchError = false; if (treeContext !== null) { restoreSuspendedTreeContext(fiber, treeContext); } @@ -396,7 +390,6 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { } function throwOnHydrationMismatch(fiber: Fiber) { - didThrowNotYetQueuedHydrationMismatchError = true; throw new Error( 'Hydration failed because the initial UI does not match what was ' + 'rendered on the server.', @@ -459,29 +452,24 @@ function prepareToHydrateHostInstance( const instance: Instance = fiber.stateNode; const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV; - try { - const updatePayload = hydrateInstance( - instance, - fiber.type, - fiber.memoizedProps, - rootContainerInstance, - hostContext, - fiber, - shouldWarnIfMismatchDev, - ); + const updatePayload = hydrateInstance( + instance, + fiber.type, + fiber.memoizedProps, + rootContainerInstance, + hostContext, + fiber, + shouldWarnIfMismatchDev, + ); - // TODO: Type this specific to this type of component. - fiber.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload !== null) { - return true; - } - return false; - } catch (error) { - didThrowNotYetQueuedHydrationMismatchError = true; - throw error; + // TODO: Type this specific to this type of component. + fiber.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. + if (updatePayload !== null) { + return true; } + return false; } function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { @@ -690,24 +678,18 @@ function getIsHydrating(): boolean { return isHydrating; } -export function queueHydrationError(error: mixed): void { +function queueIfFirstHydrationError(error: mixed): void { + if (hydrationErrors === null) { + hydrationErrors = [error]; + } +} + +function queueHydrationError(error: mixed): void { if (hydrationErrors === null) { - // We always queue the first hydration error hydrationErrors = [error]; } else { - // didThrowNotYetQueuedHydrationMismatchError will be true if we just threw - // a hydration mismatch error. In those cases we do not want to queue the - // error because there is a more useful error related to hydration that - // was already queued. - if (!didThrowNotYetQueuedHydrationMismatchError) { - // this error came from somewhere other than a hydration mismatch so we - // queue it even though other errors have also been queued. The user - // should see these errors if a recovery is made - hydrationErrors.push(error); - } + hydrationErrors.push(error); } - // Now that we have handled the queueing request we reset this - didThrowNotYetQueuedHydrationMismatchError = false; } export { @@ -723,4 +705,6 @@ export { popHydrationState, hasUnhydratedTailNodes, warnIfUnhydratedTailNodes, + queueIfFirstHydrationError, + queueHydrationError, }; diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js index 91a299058791..8b19675d2ff8 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js @@ -88,10 +88,6 @@ let didSuspendOrErrorDEV: boolean = false; // Hydration errors that were thrown inside this boundary let hydrationErrors: Array | null = null; -// When true we expect the next queued hydration error to be a mismatch error. -// These errors are handled differently than other errors that occurr during hydration -let didThrowNotYetQueuedHydrationMismatchError = false; - export function hydrationDidSuspendOrErrorDEV() { return didSuspendOrErrorDEV; } @@ -394,7 +390,6 @@ function shouldClientRenderOnMismatch(fiber: Fiber) { } function throwOnHydrationMismatch(fiber: Fiber) { - didThrowNotYetQueuedHydrationMismatchError = true; throw new Error( 'Hydration failed because the initial UI does not match what was ' + 'rendered on the server.', @@ -457,29 +452,24 @@ function prepareToHydrateHostInstance( const instance: Instance = fiber.stateNode; const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV; - try { - const updatePayload = hydrateInstance( - instance, - fiber.type, - fiber.memoizedProps, - rootContainerInstance, - hostContext, - fiber, - shouldWarnIfMismatchDev, - ); + const updatePayload = hydrateInstance( + instance, + fiber.type, + fiber.memoizedProps, + rootContainerInstance, + hostContext, + fiber, + shouldWarnIfMismatchDev, + ); - // TODO: Type this specific to this type of component. - fiber.updateQueue = (updatePayload: any); - // If the update payload indicates that there is a change or if there - // is a new ref we mark this as an update. - if (updatePayload !== null) { - return true; - } - return false; - } catch (error) { - didThrowNotYetQueuedHydrationMismatchError = true; - throw error; + // TODO: Type this specific to this type of component. + fiber.updateQueue = (updatePayload: any); + // If the update payload indicates that there is a change or if there + // is a new ref we mark this as an update. + if (updatePayload !== null) { + return true; } + return false; } function prepareToHydrateHostTextInstance(fiber: Fiber): boolean { @@ -688,24 +678,18 @@ function getIsHydrating(): boolean { return isHydrating; } -export function queueHydrationError(error: mixed): void { +function queueIfFirstHydrationError(error: mixed): void { + if (hydrationErrors === null) { + hydrationErrors = [error]; + } +} + +function queueHydrationError(error: mixed): void { if (hydrationErrors === null) { - // We always queue the first hydration error hydrationErrors = [error]; } else { - // didThrowNotYetQueuedHydrationMismatchError will be true if we just threw - // a hydration mismatch error. In those cases we do not want to queue the - // error because there is a more useful error related to hydration that - // was already queued. - if (!didThrowNotYetQueuedHydrationMismatchError) { - // this error came from somewhere other than a hydration mismatch so we - // queue it even though other errors have also been queued. The user - // should see these errors if a recovery is made - hydrationErrors.push(error); - } + hydrationErrors.push(error); } - // Now that we have handled the queueing request we reset this - didThrowNotYetQueuedHydrationMismatchError = false; } export { @@ -721,4 +705,6 @@ export { popHydrationState, hasUnhydratedTailNodes, warnIfUnhydratedTailNodes, + queueIfFirstHydrationError, + queueHydrationError, }; diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js index 7db27ba935d0..c77a52d6e630 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.new.js +++ b/packages/react-reconciler/src/ReactFiberThrow.new.js @@ -84,7 +84,7 @@ import { import { getIsHydrating, markDidThrowWhileHydratingDEV, - queueHydrationError, + queueIfFirstHydrationError, } from './ReactFiberHydrationContext.new'; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; @@ -542,7 +542,7 @@ function throwException( // Even though the user may not be affected by this error, we should // still log it so it can be fixed. - queueHydrationError(value); + queueIfFirstHydrationError(value); return; } } else { diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index a8e75e9ce661..54681242632e 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -84,8 +84,13 @@ import { import { getIsHydrating, markDidThrowWhileHydratingDEV, +<<<<<<< packages/react-reconciler/src/ReactFiberThrow.old.js queueHydrationError, } from './ReactFiberHydrationContext.old'; +======= + queueIfFirstHydrationError, +} from './ReactFiberHydrationContext.new'; +>>>>>>> packages/react-reconciler/src/ReactFiberThrow.new.js const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; @@ -542,7 +547,7 @@ function throwException( // Even though the user may not be affected by this error, we should // still log it so it can be fixed. - queueHydrationError(value); + queueIfFirstHydrationError(value); return; } } else { From 0489ee0fa578e5c6adfe9b3165c3f16938587f04 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 29 Apr 2022 14:46:57 -0700 Subject: [PATCH 12/13] improve tests --- .../src/__tests__/ReactDOMFizzServer-test.js | 167 +++++++++++++++++- 1 file changed, 158 insertions(+), 9 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 3cbd652e342a..f6a8c8105d16 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -3179,6 +3179,7 @@ describe('ReactDOMFizzServer', () => { function ThrowUntilOnClient({children, message}) { if (isClient && shouldThrow) { + Scheduler.unstable_yieldValue('throwing: ' + message); throw new Error(message); } return children; @@ -3234,9 +3235,25 @@ describe('ReactDOMFizzServer', () => { }, }); expect(Scheduler).toFlushAndYield([ + 'throwing: first error', + // this repeated first error is the invokeGuardedCallback throw + 'throwing: first error', + // these are actually thrown during render but no iGC repeat and no queueing as hydration errors + 'throwing: second error', + 'throwing: third error', + // the first hydration error cause is queued and later made recoverable 'Logged recoverable error: first error', + // other recoverable errors are queued as hydration errors 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', ]); + // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use) + // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document + expect(mockError.mock.calls).toEqual([ + // we only get one because we suppress invokeGuardedCallback after the first one when hydrating in a + // suspense boundary + ['Error: Uncaught [Error: first error]'], + ]); + mockError.mockClear(); expect(getVisibleChildren(container)).toEqual(
@@ -3247,14 +3264,7 @@ describe('ReactDOMFizzServer', () => { ); expect(Scheduler).toFlushAndYield([]); - - // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use) - // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document - expect(mockError.mock.calls).toEqual([ - // we only get one because we suppress invokeGuardedCallback after the first one when hydrating in a - // suspense boundary - ['Error: Uncaught [Error: first error]'], - ]); + expect(mockError.mock.calls).toEqual([]); } finally { console.error = originalConsoleError; } @@ -3290,6 +3300,7 @@ describe('ReactDOMFizzServer', () => { }; }); } + Scheduler.unstable_yieldValue('suspending'); throw promise; } return null; @@ -3297,6 +3308,7 @@ describe('ReactDOMFizzServer', () => { function ThrowUntilOnClient({children, message}) { if (isClient && shouldThrow) { + Scheduler.unstable_yieldValue('throwing: ' + message); throw new Error(message); } return children; @@ -3352,7 +3364,16 @@ describe('ReactDOMFizzServer', () => { ); }, }); - expect(Scheduler).toFlushAndYield([]); + expect(Scheduler).toFlushAndYield([ + 'suspending', + 'throwing: first error', + // There is no repeated first error because we already suspended and no + // invokeGuardedCallback is used if we are in dev + // or in prod there is just never an invokeGuardedCallback + 'throwing: second error', + 'throwing: third error', + ]); + expect(mockError.mock.calls).toEqual([]); expect(getVisibleChildren(container)).toEqual(
@@ -3362,10 +3383,138 @@ describe('ReactDOMFizzServer', () => {
, ); await unsuspend(); + // Since our client components only throw on the very first render there are no + // new throws in this pass expect(Scheduler).toFlushAndYield([]); + expect(mockError.mock.calls).toEqual([]); + } finally { + console.error = originalConsoleError; + } + }); + + // @gate experimental && __DEV__ + it('suspending after erroring will cause errors previously queued to be silenced until the boundary resolves', async () => { + // We can't use the toErrorDev helper here because this is async. + const originalConsoleError = console.error; + const mockError = jest.fn(); + console.error = (...args) => { + if (args.length > 1) { + if (typeof args[1] === 'object') { + mockError(args[0].split('\n')[0]); + return; + } + } + mockError(...args.map(normalizeCodeLocInfo)); + }; + let isClient = false; + let shouldThrow = true; + let promise = null; + let unsuspend = null; + let isResolved = false; + + function ComponentThatSuspendsOnClient() { + if (isClient && !isResolved) { + if (promise === null) { + promise = new Promise(resolve => { + unsuspend = () => { + isResolved = true; + resolve(); + }; + }); + } + Scheduler.unstable_yieldValue('suspending'); + throw promise; + } + return null; + } + + function ThrowUntilOnClient({children, message}) { + if (isClient && shouldThrow) { + Scheduler.unstable_yieldValue('throwing: ' + message); + throw new Error(message); + } + return children; + } + + function StopThrowingOnClient() { + if (isClient) { + shouldThrow = false; + } + return null; + } + + const App = () => { + return ( +
+ Loading...}> + +

one

+
+ +

two

+
+ + +

three

+
+ +
+
+ ); + }; + + try { + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream(); + pipe(writable); + }); + + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + + isClient = true; + + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Logged recoverable error: ' + error.message, + ); + }, + }); + expect(Scheduler).toFlushAndYield([ + 'throwing: first error', + // duplicate because first error is re-done in invokeGuardedCallback + 'throwing: first error', + 'throwing: second error', + 'suspending', + 'throwing: third error', + ]); // These Uncaught error calls are the error reported by the runtime (jsdom here, browser in actual use) // when invokeGuardedCallback is used to replay an error in dev using event dispatching in the document + expect(mockError.mock.calls).toEqual([ + // we only get one because we suppress invokeGuardedCallback after the first one when hydrating in a + // suspense boundary + ['Error: Uncaught [Error: first error]'], + ]); + mockError.mockClear(); + + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + await unsuspend(); + // Since our client components only throw on the very first render there are no + // new throws in this pass + expect(Scheduler).toFlushAndYield([]); expect(mockError.mock.calls).toEqual([]); } finally { console.error = originalConsoleError; From d016d50e633bb8c822db30b265b49f5fd902f155 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 29 Apr 2022 14:48:05 -0700 Subject: [PATCH 13/13] fix forks --- packages/react-reconciler/src/ReactFiberThrow.old.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js index 54681242632e..f98252154ef0 100644 --- a/packages/react-reconciler/src/ReactFiberThrow.old.js +++ b/packages/react-reconciler/src/ReactFiberThrow.old.js @@ -84,13 +84,8 @@ import { import { getIsHydrating, markDidThrowWhileHydratingDEV, -<<<<<<< packages/react-reconciler/src/ReactFiberThrow.old.js - queueHydrationError, -} from './ReactFiberHydrationContext.old'; -======= queueIfFirstHydrationError, -} from './ReactFiberHydrationContext.new'; ->>>>>>> packages/react-reconciler/src/ReactFiberThrow.new.js +} from './ReactFiberHydrationContext.old'; const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;