Skip to content

Commit 67dd1a2

Browse files
committed
Don't warn on hydration mismatch if suspended
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
1 parent 33cfef8 commit 67dd1a2

7 files changed

+82
-26
lines changed

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -302,18 +302,14 @@ describe('ReactDOMFizzServer', () => {
302302
});
303303

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

313-
// Test passes if you change:
314-
// <span id="after">after</span>
315-
// to:
316-
// <Suspense fallback={null}><span id="after">after</span></Suspense>
317313
const App = () => (
318314
<div>
319315
<Suspense fallback={<span>Loading...</span>}>
@@ -351,16 +347,8 @@ describe('ReactDOMFizzServer', () => {
351347
const [HydrateApp, hydrateResolve] = makeApp();
352348
await act(async () => {
353349
ReactDOM.hydrateRoot(container, <HydrateApp />);
354-
// Throws after flushAll:
355-
// Warning: Prop `id` did not match. Server: "async" Client: "after"
356-
// at span
357-
// at Suspense
358-
// at div
359-
// at App
360-
Scheduler.unstable_flushAll();
361350
});
362351

363-
// nb: Honestly not really sure whether this should expect "loading..." or "async"
364352
expect(getVisibleChildren(container)).toEqual(
365353
<div>
366354
<span id="async">async</span>

packages/react-dom/src/client/ReactDOMComponent.js

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -230,21 +230,24 @@ export function checkForUnmatchedText(
230230
serverText: string,
231231
clientText: string | number,
232232
isConcurrentMode: boolean,
233+
shouldWarnDev: boolean,
233234
) {
234235
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
235236
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
236237
if (normalizedServerText === normalizedClientText) {
237238
return;
238239
}
239240

240-
if (__DEV__) {
241-
if (!didWarnInvalidHydration) {
242-
didWarnInvalidHydration = true;
243-
console.error(
244-
'Text content did not match. Server: "%s" Client: "%s"',
245-
normalizedServerText,
246-
normalizedClientText,
247-
);
241+
if (shouldWarnDev) {
242+
if (__DEV__) {
243+
if (!didWarnInvalidHydration) {
244+
didWarnInvalidHydration = true;
245+
console.error(
246+
'Text content did not match. Server: "%s" Client: "%s"',
247+
normalizedServerText,
248+
normalizedClientText,
249+
);
250+
}
248251
}
249252
}
250253

@@ -866,6 +869,7 @@ export function diffHydratedProperties(
866869
parentNamespace: string,
867870
rootContainerElement: Element | Document,
868871
isConcurrentMode: boolean,
872+
shouldWarnDev: boolean,
869873
): null | Array<mixed> {
870874
let isCustomComponentTag;
871875
let extraAttributeNames: Set<string>;
@@ -985,6 +989,7 @@ export function diffHydratedProperties(
985989
domElement.textContent,
986990
nextProp,
987991
isConcurrentMode,
992+
shouldWarnDev,
988993
);
989994
}
990995
updatePayload = [CHILDREN, nextProp];
@@ -996,6 +1001,7 @@ export function diffHydratedProperties(
9961001
domElement.textContent,
9971002
nextProp,
9981003
isConcurrentMode,
1004+
shouldWarnDev,
9991005
);
10001006
}
10011007
updatePayload = [CHILDREN, '' + nextProp];
@@ -1011,6 +1017,7 @@ export function diffHydratedProperties(
10111017
}
10121018
}
10131019
} else if (
1020+
shouldWarnDev &&
10141021
__DEV__ &&
10151022
// Convince Flow we've calculated it (it's DEV-only in this method.)
10161023
typeof isCustomComponentTag === 'boolean'
@@ -1142,10 +1149,12 @@ export function diffHydratedProperties(
11421149
}
11431150

11441151
if (__DEV__) {
1145-
// $FlowFixMe - Should be inferred as not undefined.
1146-
if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
1152+
if (shouldWarnDev) {
11471153
// $FlowFixMe - Should be inferred as not undefined.
1148-
warnForExtraAttributes(extraAttributeNames);
1154+
if (extraAttributeNames.size > 0 && !suppressHydrationWarning) {
1155+
// $FlowFixMe - Should be inferred as not undefined.
1156+
warnForExtraAttributes(extraAttributeNames);
1157+
}
11491158
}
11501159
}
11511160

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,7 @@ export function hydrateInstance(
786786
rootContainerInstance: Container,
787787
hostContext: HostContext,
788788
internalInstanceHandle: Object,
789+
shouldWarnDev: boolean,
789790
): null | Array<mixed> {
790791
precacheFiberNode(internalInstanceHandle, instance);
791792
// TODO: Possibly defer this until the commit phase where all the events
@@ -811,13 +812,15 @@ export function hydrateInstance(
811812
parentNamespace,
812813
rootContainerInstance,
813814
isConcurrentMode,
815+
shouldWarnDev,
814816
);
815817
}
816818

817819
export function hydrateTextInstance(
818820
textInstance: TextInstance,
819821
text: string,
820822
internalInstanceHandle: Object,
823+
shouldWarnDev: boolean,
821824
): boolean {
822825
precacheFiberNode(internalInstanceHandle, textInstance);
823826

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
8484
let hydrationParentFiber: null | Fiber = null;
8585
let nextHydratableInstance: null | HydratableInstance = null;
8686
let isHydrating: boolean = false;
87+
let didSuspend: boolean = false;
8788

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

102+
export function markDidSuspendWhileHydratingDEV() {
103+
if (__DEV__) {
104+
didSuspend = true;
105+
}
106+
}
107+
101108
function enterHydrationState(fiber: Fiber): boolean {
102109
if (!supportsHydration) {
103110
return false;
@@ -110,6 +117,7 @@ function enterHydrationState(fiber: Fiber): boolean {
110117
hydrationParentFiber = fiber;
111118
isHydrating = true;
112119
hydrationErrors = null;
120+
didSuspend = false;
113121
return true;
114122
}
115123

@@ -127,6 +135,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
127135
hydrationParentFiber = fiber;
128136
isHydrating = true;
129137
hydrationErrors = null;
138+
didSuspend = false;
130139
if (treeContext !== null) {
131140
restoreSuspendedTreeContext(fiber, treeContext);
132141
}
@@ -185,6 +194,13 @@ function deleteHydratableInstance(
185194

186195
function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
187196
if (__DEV__) {
197+
if (didSuspend) {
198+
// Inside a boundary that already suspended. We're currently rendering the
199+
// siblings of a suspended node. The mismatch may be due to the missing
200+
// data, so it's probably a false positive.
201+
return;
202+
}
203+
188204
switch (returnFiber.tag) {
189205
case HostRoot: {
190206
const parentContainer = returnFiber.stateNode.containerInfo;
@@ -418,13 +434,15 @@ function prepareToHydrateHostInstance(
418434
}
419435

420436
const instance: Instance = fiber.stateNode;
437+
const shouldWarnIfMismatchDev = didSuspend;
421438
const updatePayload = hydrateInstance(
422439
instance,
423440
fiber.type,
424441
fiber.memoizedProps,
425442
rootContainerInstance,
426443
hostContext,
427444
fiber,
445+
shouldWarnIfMismatchDev,
428446
);
429447
// TODO: Type this specific to this type of component.
430448
fiber.updateQueue = (updatePayload: any);
@@ -446,7 +464,13 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
446464

447465
const textInstance: TextInstance = fiber.stateNode;
448466
const textContent: string = fiber.memoizedProps;
449-
const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber);
467+
const shouldWarnIfMismatchDev = didSuspend;
468+
const shouldUpdate = hydrateTextInstance(
469+
textInstance,
470+
textContent,
471+
fiber,
472+
shouldWarnIfMismatchDev,
473+
);
450474
if (shouldUpdate) {
451475
// We assume that prepareToHydrateHostTextInstance is called in a context where the
452476
// hydration parent is the parent host component of this host text.
@@ -616,6 +640,7 @@ function resetHydrationState(): void {
616640
hydrationParentFiber = null;
617641
nextHydratableInstance = null;
618642
isHydrating = false;
643+
didSuspend = false;
619644
}
620645

621646
export function upgradeHydrationErrorsToRecoverable(): void {

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
8484
let hydrationParentFiber: null | Fiber = null;
8585
let nextHydratableInstance: null | HydratableInstance = null;
8686
let isHydrating: boolean = false;
87+
let didSuspend: boolean = false;
8788

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

102+
export function markDidSuspendWhileHydratingDEV() {
103+
if (__DEV__) {
104+
didSuspend = true;
105+
}
106+
}
107+
101108
function enterHydrationState(fiber: Fiber): boolean {
102109
if (!supportsHydration) {
103110
return false;
@@ -110,6 +117,7 @@ function enterHydrationState(fiber: Fiber): boolean {
110117
hydrationParentFiber = fiber;
111118
isHydrating = true;
112119
hydrationErrors = null;
120+
didSuspend = false;
113121
return true;
114122
}
115123

@@ -127,6 +135,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
127135
hydrationParentFiber = fiber;
128136
isHydrating = true;
129137
hydrationErrors = null;
138+
didSuspend = false;
130139
if (treeContext !== null) {
131140
restoreSuspendedTreeContext(fiber, treeContext);
132141
}
@@ -185,6 +194,13 @@ function deleteHydratableInstance(
185194

186195
function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
187196
if (__DEV__) {
197+
if (didSuspend) {
198+
// Inside a boundary that already suspended. We're currently rendering the
199+
// siblings of a suspended node. The mismatch may be due to the missing
200+
// data, so it's probably a false positive.
201+
return;
202+
}
203+
188204
switch (returnFiber.tag) {
189205
case HostRoot: {
190206
const parentContainer = returnFiber.stateNode.containerInfo;
@@ -418,13 +434,15 @@ function prepareToHydrateHostInstance(
418434
}
419435

420436
const instance: Instance = fiber.stateNode;
437+
const shouldWarnIfMismatchDev = didSuspend;
421438
const updatePayload = hydrateInstance(
422439
instance,
423440
fiber.type,
424441
fiber.memoizedProps,
425442
rootContainerInstance,
426443
hostContext,
427444
fiber,
445+
shouldWarnIfMismatchDev,
428446
);
429447
// TODO: Type this specific to this type of component.
430448
fiber.updateQueue = (updatePayload: any);
@@ -446,7 +464,13 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
446464

447465
const textInstance: TextInstance = fiber.stateNode;
448466
const textContent: string = fiber.memoizedProps;
449-
const shouldUpdate = hydrateTextInstance(textInstance, textContent, fiber);
467+
const shouldWarnIfMismatchDev = didSuspend;
468+
const shouldUpdate = hydrateTextInstance(
469+
textInstance,
470+
textContent,
471+
fiber,
472+
shouldWarnIfMismatchDev,
473+
);
450474
if (shouldUpdate) {
451475
// We assume that prepareToHydrateHostTextInstance is called in a context where the
452476
// hydration parent is the parent host component of this host text.
@@ -616,6 +640,7 @@ function resetHydrationState(): void {
616640
hydrationParentFiber = null;
617641
nextHydratableInstance = null;
618642
isHydrating = false;
643+
didSuspend = false;
619644
}
620645

621646
export function upgradeHydrationErrorsToRecoverable(): void {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
} from './ReactFiberLane.new';
8484
import {
8585
getIsHydrating,
86+
markDidSuspendWhileHydratingDEV,
8687
queueHydrationError,
8788
} from './ReactFiberHydrationContext.new';
8889

@@ -513,6 +514,8 @@ function throwException(
513514
} else {
514515
// This is a regular error, not a Suspense wakeable.
515516
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
517+
markDidSuspendWhileHydratingDEV();
518+
516519
const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
517520
// If the error was thrown during hydration, we may be able to recover by
518521
// discarding the dehydrated content and switching to a client render.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
} from './ReactFiberLane.old';
8484
import {
8585
getIsHydrating,
86+
markDidSuspendWhileHydratingDEV,
8687
queueHydrationError,
8788
} from './ReactFiberHydrationContext.old';
8889

@@ -513,6 +514,8 @@ function throwException(
513514
} else {
514515
// This is a regular error, not a Suspense wakeable.
515516
if (getIsHydrating() && sourceFiber.mode & ConcurrentMode) {
517+
markDidSuspendWhileHydratingDEV();
518+
516519
const suspenseBoundary = getNearestSuspenseBoundaryToCapture(returnFiber);
517520
// If the error was thrown during hydration, we may be able to recover by
518521
// discarding the dehydrated content and switching to a client render.

0 commit comments

Comments
 (0)