Skip to content

Commit

Permalink
Avoid accumulating hydration mismatch errors after the first hydratio…
Browse files Browse the repository at this point in the history
…n 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
  • Loading branch information
gnoff committed Apr 22, 2022
1 parent bd08137 commit 4db23e0
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 22 deletions.
4 changes: 0 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Expand Up @@ -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.',
]);
Expand Down Expand Up @@ -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.',
]);

Expand Down
58 changes: 40 additions & 18 deletions packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}
}

Expand Down

0 comments on commit 4db23e0

Please sign in to comment.