Skip to content

Commit

Permalink
Don't warn on hydration mismatch if suspended
Browse files Browse the repository at this point in the history
When something suspends during hydration, we continue rendering the
siblings to warm up the cache and fire off any lazy network requests.
However, if there are any mismatches while rendering the siblings, it's
likely a false positive caused by the earlier suspended component. So
we should suppress any hydration warnings until the tree no
longer suspends.

Fixes #23332
  • Loading branch information
acdlite committed Feb 25, 2022
1 parent 33cfef8 commit 67dd1a2
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 26 deletions.
14 changes: 1 addition & 13 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,18 +302,14 @@ describe('ReactDOMFizzServer', () => {
});

// @gate experimental
it('should asynchronously load a lazy component with sibling after', async () => {
it('#23331: does not warn about hydration mismatches if something suspended in an earlier sibling', async () => {
const makeApp = () => {
let resolve;
const imports = new Promise(r => {
resolve = () => r({default: () => <span id="async">async</span>});
});
const Lazy = React.lazy(() => imports);

// Test passes if you change:
// <span id="after">after</span>
// to:
// <Suspense fallback={null}><span id="after">after</span></Suspense>
const App = () => (
<div>
<Suspense fallback={<span>Loading...</span>}>
Expand Down Expand Up @@ -351,16 +347,8 @@ describe('ReactDOMFizzServer', () => {
const [HydrateApp, hydrateResolve] = makeApp();
await act(async () => {
ReactDOM.hydrateRoot(container, <HydrateApp />);
// Throws after flushAll:
// Warning: Prop `id` did not match. Server: "async" Client: "after"
// at span
// at Suspense
// at div
// at App
Scheduler.unstable_flushAll();
});

// nb: Honestly not really sure whether this should expect "loading..." or "async"
expect(getVisibleChildren(container)).toEqual(
<div>
<span id="async">async</span>
Expand Down
31 changes: 20 additions & 11 deletions packages/react-dom/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,24 @@ export function checkForUnmatchedText(
serverText: string,
clientText: string | number,
isConcurrentMode: boolean,
shouldWarnDev: boolean,
) {
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
if (normalizedServerText === normalizedClientText) {
return;
}

if (__DEV__) {
if (!didWarnInvalidHydration) {
didWarnInvalidHydration = true;
console.error(
'Text content did not match. Server: "%s" Client: "%s"',
normalizedServerText,
normalizedClientText,
);
if (shouldWarnDev) {
if (__DEV__) {
if (!didWarnInvalidHydration) {
didWarnInvalidHydration = true;
console.error(
'Text content did not match. Server: "%s" Client: "%s"',
normalizedServerText,
normalizedClientText,
);
}
}
}

Expand Down Expand Up @@ -866,6 +869,7 @@ export function diffHydratedProperties(
parentNamespace: string,
rootContainerElement: Element | Document,
isConcurrentMode: boolean,
shouldWarnDev: boolean,
): null | Array<mixed> {
let isCustomComponentTag;
let extraAttributeNames: Set<string>;
Expand Down Expand Up @@ -985,6 +989,7 @@ export function diffHydratedProperties(
domElement.textContent,
nextProp,
isConcurrentMode,
shouldWarnDev,
);
}
updatePayload = [CHILDREN, nextProp];
Expand All @@ -996,6 +1001,7 @@ export function diffHydratedProperties(
domElement.textContent,
nextProp,
isConcurrentMode,
shouldWarnDev,
);
}
updatePayload = [CHILDREN, '' + nextProp];
Expand All @@ -1011,6 +1017,7 @@ export function diffHydratedProperties(
}
}
} else if (
shouldWarnDev &&
__DEV__ &&
// Convince Flow we've calculated it (it's DEV-only in this method.)
typeof isCustomComponentTag === 'boolean'
Expand Down Expand Up @@ -1142,10 +1149,12 @@ export function diffHydratedProperties(
}

if (__DEV__) {
// $FlowFixMe - Should be inferred as not undefined.
if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
if (shouldWarnDev) {
// $FlowFixMe - Should be inferred as not undefined.
warnForExtraAttributes(extraAttributeNames);
if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
// $FlowFixMe - Should be inferred as not undefined.
warnForExtraAttributes(extraAttributeNames);
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ export function hydrateInstance(
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
shouldWarnDev: boolean,
): null | Array<mixed> {
precacheFiberNode(internalInstanceHandle, instance);
// TODO: Possibly defer this until the commit phase where all the events
Expand All @@ -811,13 +812,15 @@ export function hydrateInstance(
parentNamespace,
rootContainerInstance,
isConcurrentMode,
shouldWarnDev,
);
}

export function hydrateTextInstance(
textInstance: TextInstance,
text: string,
internalInstanceHandle: Object,
shouldWarnDev: boolean,
): boolean {
precacheFiberNode(internalInstanceHandle, textInstance);

Expand Down
27 changes: 26 additions & 1 deletion packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
let hydrationParentFiber: null | Fiber = null;
let nextHydratableInstance: null | HydratableInstance = null;
let isHydrating: boolean = false;
let didSuspend: boolean = false;

// Hydration errors that were thrown inside this boundary
let hydrationErrors: Array<mixed> | null = null;
Expand All @@ -98,6 +99,12 @@ function warnIfHydrating() {
}
}

export function markDidSuspendWhileHydratingDEV() {
if (__DEV__) {
didSuspend = true;
}
}

function enterHydrationState(fiber: Fiber): boolean {
if (!supportsHydration) {
return false;
Expand All @@ -110,6 +117,7 @@ function enterHydrationState(fiber: Fiber): boolean {
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
didSuspend = false;
return true;
}

Expand All @@ -127,6 +135,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
didSuspend = false;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
Expand Down Expand Up @@ -185,6 +194,13 @@ function deleteHydratableInstance(

function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
if (__DEV__) {
if (didSuspend) {
// Inside a boundary that already suspended. We're currently rendering the
// siblings of a suspended node. The mismatch may be due to the missing
// data, so it's probably a false positive.
return;
}

switch (returnFiber.tag) {
case HostRoot: {
const parentContainer = returnFiber.stateNode.containerInfo;
Expand Down Expand Up @@ -418,13 +434,15 @@ function prepareToHydrateHostInstance(
}

const instance: Instance = fiber.stateNode;
const shouldWarnIfMismatchDev = didSuspend;
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);
Expand All @@ -446,7 +464,13 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {

const textInstance: TextInstance = fiber.stateNode;
const textContent: string = fiber.memoizedProps;
const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber);
const shouldWarnIfMismatchDev = didSuspend;
const shouldUpdate = hydrateTextInstance(
textInstance,
textContent,
fiber,
shouldWarnIfMismatchDev,
);
if (shouldUpdate) {
// We assume that prepareToHydrateHostTextInstance is called in a context where the
// hydration parent is the parent host component of this host text.
Expand Down Expand Up @@ -616,6 +640,7 @@ function resetHydrationState(): void {
hydrationParentFiber = null;
nextHydratableInstance = null;
isHydrating = false;
didSuspend = false;
}

export function upgradeHydrationErrorsToRecoverable(): void {
Expand Down
27 changes: 26 additions & 1 deletion packages/react-reconciler/src/ReactFiberHydrationContext.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
let hydrationParentFiber: null | Fiber = null;
let nextHydratableInstance: null | HydratableInstance = null;
let isHydrating: boolean = false;
let didSuspend: boolean = false;

// Hydration errors that were thrown inside this boundary
let hydrationErrors: Array<mixed> | null = null;
Expand All @@ -98,6 +99,12 @@ function warnIfHydrating() {
}
}

export function markDidSuspendWhileHydratingDEV() {
if (__DEV__) {
didSuspend = true;
}
}

function enterHydrationState(fiber: Fiber): boolean {
if (!supportsHydration) {
return false;
Expand All @@ -110,6 +117,7 @@ function enterHydrationState(fiber: Fiber): boolean {
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
didSuspend = false;
return true;
}

Expand All @@ -127,6 +135,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
hydrationParentFiber = fiber;
isHydrating = true;
hydrationErrors = null;
didSuspend = false;
if (treeContext !== null) {
restoreSuspendedTreeContext(fiber, treeContext);
}
Expand Down Expand Up @@ -185,6 +194,13 @@ function deleteHydratableInstance(

function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
if (__DEV__) {
if (didSuspend) {
// Inside a boundary that already suspended. We're currently rendering the
// siblings of a suspended node. The mismatch may be due to the missing
// data, so it's probably a false positive.
return;
}

switch (returnFiber.tag) {
case HostRoot: {
const parentContainer = returnFiber.stateNode.containerInfo;
Expand Down Expand Up @@ -418,13 +434,15 @@ function prepareToHydrateHostInstance(
}

const instance: Instance = fiber.stateNode;
const shouldWarnIfMismatchDev = didSuspend;
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);
Expand All @@ -446,7 +464,13 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {

const textInstance: TextInstance = fiber.stateNode;
const textContent: string = fiber.memoizedProps;
const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber);
const shouldWarnIfMismatchDev = didSuspend;
const shouldUpdate = hydrateTextInstance(
textInstance,
textContent,
fiber,
shouldWarnIfMismatchDev,
);
if (shouldUpdate) {
// We assume that prepareToHydrateHostTextInstance is called in a context where the
// hydration parent is the parent host component of this host text.
Expand Down Expand Up @@ -616,6 +640,7 @@ function resetHydrationState(): void {
hydrationParentFiber = null;
nextHydratableInstance = null;
isHydrating = false;
didSuspend = false;
}

export function upgradeHydrationErrorsToRecoverable(): void {
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberThrow.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
} from './ReactFiberLane.new';
import {
getIsHydrating,
markDidSuspendWhileHydratingDEV,
queueHydrationError,
} from './ReactFiberHydrationContext.new';

Expand Down Expand Up @@ -513,6 +514,8 @@ function throwException(
} else {
// This is a regular error, not a Suspense wakeable.
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
markDidSuspendWhileHydratingDEV();

const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
// If the error was thrown during hydration, we may be able to recover by
// discarding the dehydrated content and switching to a client render.
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactFiberThrow.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
} from './ReactFiberLane.old';
import {
getIsHydrating,
markDidSuspendWhileHydratingDEV,
queueHydrationError,
} from './ReactFiberHydrationContext.old';

Expand Down Expand Up @@ -513,6 +514,8 @@ function throwException(
} else {
// This is a regular error, not a Suspense wakeable.
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
markDidSuspendWhileHydratingDEV();

const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
// If the error was thrown during hydration, we may be able to recover by
// discarding the dehydrated content and switching to a client render.
Expand Down

0 comments on commit 67dd1a2

Please sign in to comment.