Skip to content

Commit 0f8775a

Browse files
committed
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
1 parent bd08137 commit 0f8775a

File tree

3 files changed

+80
-40
lines changed

3 files changed

+80
-40
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2976,8 +2976,6 @@ describe('ReactDOMFizzServer', () => {
29762976
},
29772977
});
29782978
expect(Scheduler).toFlushAndYield([
2979-
'Logged recoverable error: Text content does not match server-rendered HTML.',
2980-
'Logged recoverable error: Text content does not match server-rendered HTML.',
29812979
'Logged recoverable error: Text content does not match server-rendered HTML.',
29822980
'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
29832981
]);
@@ -3069,8 +3067,6 @@ describe('ReactDOMFizzServer', () => {
30693067
});
30703068
expect(Scheduler).toFlushAndYield([
30713069
'Logged recoverable error: uh oh',
3072-
'Logged recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
3073-
'Logged recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
30743070
'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.',
30753071
]);
30763072

packages/react-reconciler/src/ReactFiberHydrationContext.new.js

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,12 @@ function shouldClientRenderOnMismatch(fiber: Fiber) {
386386
}
387387

388388
function throwOnHydrationMismatch(fiber: Fiber) {
389-
throw new Error(
389+
const error = new Error(
390390
'Hydration failed because the initial UI does not match what was ' +
391391
'rendered on the server.',
392392
);
393+
(error: any)._hydrationMismatch = true;
394+
throw error;
393395
}
394396

395397
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
@@ -448,23 +450,32 @@ function prepareToHydrateHostInstance(
448450

449451
const instance: Instance = fiber.stateNode;
450452
const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV;
451-
const updatePayload = hydrateInstance(
452-
instance,
453-
fiber.type,
454-
fiber.memoizedProps,
455-
rootContainerInstance,
456-
hostContext,
457-
fiber,
458-
shouldWarnIfMismatchDev,
459-
);
460-
// TODO: Type this specific to this type of component.
461-
fiber.updateQueue = (updatePayload: any);
462-
// If the update payload indicates that there is a change or if there
463-
// is a new ref we mark this as an update.
464-
if (updatePayload !== null) {
465-
return true;
453+
try {
454+
const updatePayload = hydrateInstance(
455+
instance,
456+
fiber.type,
457+
fiber.memoizedProps,
458+
rootContainerInstance,
459+
hostContext,
460+
fiber,
461+
shouldWarnIfMismatchDev,
462+
);
463+
464+
// TODO: Type this specific to this type of component.
465+
fiber.updateQueue = (updatePayload: any);
466+
// If the update payload indicates that there is a change or if there
467+
// is a new ref we mark this as an update.
468+
if (updatePayload !== null) {
469+
return true;
470+
}
471+
return false;
472+
} catch (error) {
473+
// We use an expando to decorate errors arising from hydration matching
474+
// so we can optionally discard them if a more fundamental preceding
475+
// hydration error has occurred.
476+
error._hydrationMismatch = true;
477+
throw error;
466478
}
467-
return false;
468479
}
469480

470481
function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
@@ -675,9 +686,20 @@ function getIsHydrating(): boolean {
675686

676687
export function queueHydrationError(error: mixed): void {
677688
if (hydrationErrors === null) {
689+
// We always queue the first hydration error
678690
hydrationErrors = [error];
679691
} else {
680-
hydrationErrors.push(error);
692+
if ((error: any)._hydrationMismatch !== true) {
693+
// If there is at least one hydrationError we suppress additional
694+
// hydrationMismatch errors on the logic that the earlier error
695+
// will lead to a cascade of mismatch errors.
696+
// If the boundary is suspending then there will be another hydration
697+
// opportunity in a future render.
698+
// If the boundary is not suspending then the earlier errors are
699+
// the proximal cause and the mismatch errors are almost certainly
700+
// a distraction
701+
hydrationErrors.push(error);
702+
}
681703
}
682704
}
683705

packages/react-reconciler/src/ReactFiberHydrationContext.old.js

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -386,10 +386,12 @@ function shouldClientRenderOnMismatch(fiber: Fiber) {
386386
}
387387

388388
function throwOnHydrationMismatch(fiber: Fiber) {
389-
throw new Error(
389+
const error = new Error(
390390
'Hydration failed because the initial UI does not match what was ' +
391391
'rendered on the server.',
392392
);
393+
(error: any)._hydrationMismatch = true;
394+
throw error;
393395
}
394396

395397
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
@@ -448,23 +450,32 @@ function prepareToHydrateHostInstance(
448450

449451
const instance: Instance = fiber.stateNode;
450452
const shouldWarnIfMismatchDev = !didSuspendOrErrorDEV;
451-
const updatePayload = hydrateInstance(
452-
instance,
453-
fiber.type,
454-
fiber.memoizedProps,
455-
rootContainerInstance,
456-
hostContext,
457-
fiber,
458-
shouldWarnIfMismatchDev,
459-
);
460-
// TODO: Type this specific to this type of component.
461-
fiber.updateQueue = (updatePayload: any);
462-
// If the update payload indicates that there is a change or if there
463-
// is a new ref we mark this as an update.
464-
if (updatePayload !== null) {
465-
return true;
453+
try {
454+
const updatePayload = hydrateInstance(
455+
instance,
456+
fiber.type,
457+
fiber.memoizedProps,
458+
rootContainerInstance,
459+
hostContext,
460+
fiber,
461+
shouldWarnIfMismatchDev,
462+
);
463+
464+
// TODO: Type this specific to this type of component.
465+
fiber.updateQueue = (updatePayload: any);
466+
// If the update payload indicates that there is a change or if there
467+
// is a new ref we mark this as an update.
468+
if (updatePayload !== null) {
469+
return true;
470+
}
471+
return false;
472+
} catch (error) {
473+
// We use an expando to decorate errors arising from hydration matching
474+
// so we can optionally discard them if a more fundamental preceding
475+
// hydration error has occurred.
476+
error._hydrationMismatch = true;
477+
throw error;
466478
}
467-
return false;
468479
}
469480

470481
function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
@@ -675,9 +686,20 @@ function getIsHydrating(): boolean {
675686

676687
export function queueHydrationError(error: mixed): void {
677688
if (hydrationErrors === null) {
689+
// We always queue the first hydration error
678690
hydrationErrors = [error];
679691
} else {
680-
hydrationErrors.push(error);
692+
if ((error: any)._hydrationMismatch !== true) {
693+
// If there is at least one hydrationError we suppress additional
694+
// hydrationMismatch errors on the logic that the earlier error
695+
// will lead to a cascade of mismatch errors.
696+
// If the boundary is suspending then there will be another hydration
697+
// opportunity in a future render.
698+
// If the boundary is not suspending then the earlier errors are
699+
// the proximal cause and the mismatch errors are almost certainly
700+
// a distraction
701+
hydrationErrors.push(error);
702+
}
681703
}
682704
}
683705

0 commit comments

Comments
 (0)