From 4db23e059b983045a1710c29cef2895d2ba18a26 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Fri, 22 Apr 2022 14:40:59 -0700 Subject: [PATCH] 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 +++++++++++++------ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index bf5388597571e..1ce7c7bc75a32 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 e0e592d32790f..c787363deaac8 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._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._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); + } } }