.\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ ],
+ {withoutStack: 1},
);
expect(getVisibleChildren(container)).toEqual(
client
);
} else {
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index e071a36737104..586901baa14c3 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -19,6 +19,15 @@ let SuspenseList;
let act;
let IdleEventPriority;
+function normalizeCodeLocInfo(strOrErr) {
+ if (strOrErr && strOrErr.replace) {
+ return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
+ return '\n in ' + name + ' (at **)';
+ });
+ }
+ return strOrErr;
+}
+
function dispatchMouseEvent(to, from) {
if (!to) {
to = null;
@@ -240,6 +249,12 @@ describe('ReactDOMServerPartialHydration', () => {
// @gate enableClientRenderFallbackOnHydrationMismatch
it('falls back to client rendering boundary on mismatch', async () => {
+ // We can't use the toErrorDev helper here because this is async.
+ const originalConsoleError = console.error;
+ const mockError = jest.fn();
+ console.error = (...args) => {
+ mockError(...args.map(normalizeCodeLocInfo));
+ };
let client = false;
let suspend = false;
let resolve;
@@ -276,70 +291,86 @@ describe('ReactDOMServerPartialHydration', () => {
);
}
- const finalHTML = ReactDOMServer.renderToString(
);
- const container = document.createElement('div');
- container.innerHTML = finalHTML;
- expect(Scheduler).toHaveYielded([
- 'Hello',
- 'Component',
- 'Component',
- 'Component',
- 'Component',
- ]);
+ try {
+ const finalHTML = ReactDOMServer.renderToString(
);
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
+ expect(Scheduler).toHaveYielded([
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ ]);
- expect(container.innerHTML).toBe(
- 'Hello
Component
Component
Component
Component
',
- );
+ expect(container.innerHTML).toBe(
+ 'Hello
Component
Component
Component
Component
',
+ );
- suspend = true;
- client = true;
+ suspend = true;
+ client = true;
- ReactDOM.hydrateRoot(container,
, {
- onRecoverableError(error) {
- Scheduler.unstable_yieldValue(error.message);
- },
- });
- expect(Scheduler).toFlushAndYield([
- 'Suspend',
- 'Component',
- 'Component',
- 'Component',
- 'Component',
- ]);
- jest.runAllTimers();
+ ReactDOM.hydrateRoot(container,
, {
+ onRecoverableError(error) {
+ Scheduler.unstable_yieldValue(error.message);
+ },
+ });
+ expect(Scheduler).toFlushAndYield([
+ 'Suspend',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ ]);
+ jest.runAllTimers();
- // Unchanged
- expect(container.innerHTML).toBe(
- 'Hello
Component
Component
Component
Component
',
- );
+ // Unchanged
+ expect(container.innerHTML).toBe(
+ 'Hello
Component
Component
Component
Component
',
+ );
- suspend = false;
- resolve();
- await promise;
+ suspend = false;
+ resolve();
+ await promise;
+ expect(Scheduler).toFlushAndYield([
+ // first pass, mismatches at end
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+
+ // second pass as client render
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+
+ // Hydration mismatch is logged
+ 'An error occurred during hydration. The server HTML was replaced with client content',
+ ]);
- expect(Scheduler).toFlushAndYield([
- // first pass, mismatches at end
- 'Hello',
- 'Component',
- 'Component',
- 'Component',
- 'Component',
-
- // second pass as client render
- 'Hello',
- 'Component',
- 'Component',
- 'Component',
- 'Component',
-
- // Hydration mismatch is logged
- 'An error occurred during hydration. The server HTML was replaced with client content',
- ]);
+ // Client rendered - suspense comment nodes removed
+ expect(container.innerHTML).toBe(
+ 'Hello
Component
Component
Component
Mismatch',
+ );
- // Client rendered - suspense comment nodes removed
- expect(container.innerHTML).toBe(
- 'Hello
Component
Component
Component
Mismatch',
- );
+ if (__DEV__) {
+ expect(mockError.mock.calls[0]).toEqual([
+ 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
+ 'div',
+ 'div',
+ '\n' +
+ ' in div (at **)\n' +
+ ' in Component (at **)\n' +
+ ' in Suspense (at **)\n' +
+ ' in App (at **)',
+ ]);
+ }
+ } finally {
+ console.error = originalConsoleError;
+ }
});
it('calls the hydration callbacks after hydration or deletion', async () => {
@@ -493,21 +524,14 @@ describe('ReactDOMServerPartialHydration', () => {
});
it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
- spyOnDev(console, 'error');
- const ref = React.createRef();
- function App({hasB}) {
- return (
-
-
-
- A
- {hasB ? B : null}
-
-
Sibling
-
- );
- }
+ // We can't use the toErrorDev helper here because this is async.
+ const originalConsoleError = console.error;
+ const mockError = jest.fn();
+ console.error = (...args) => {
+ mockError(...args.map(normalizeCodeLocInfo));
+ };
+ const ref = React.createRef();
let shouldSuspend = false;
let resolve;
const promise = new Promise(res => {
@@ -522,37 +546,61 @@ describe('ReactDOMServerPartialHydration', () => {
}
return <>>;
}
+ function App({hasB}) {
+ return (
+
+
+
+ A
+ {hasB ? B : null}
+
+
Sibling
+
+ );
+ }
+ try {
+ const finalHTML = ReactDOMServer.renderToString(
);
- const finalHTML = ReactDOMServer.renderToString(
);
-
- const container = document.createElement('div');
- container.innerHTML = finalHTML;
+ const container = document.createElement('div');
+ container.innerHTML = finalHTML;
- const span = container.getElementsByTagName('span')[0];
+ const span = container.getElementsByTagName('span')[0];
- expect(container.innerHTML).toContain('
A');
- expect(container.innerHTML).toContain('
B');
- expect(ref.current).toBe(null);
+ expect(container.innerHTML).toContain('
A');
+ expect(container.innerHTML).toContain('
B');
+ expect(ref.current).toBe(null);
- shouldSuspend = true;
- act(() => {
- ReactDOM.hydrateRoot(container,
);
- });
+ shouldSuspend = true;
+ act(() => {
+ ReactDOM.hydrateRoot(container,
);
+ });
- // await expect(async () => {
- resolve();
- await promise;
- Scheduler.unstable_flushAll();
- await null;
- jest.runAllTimers();
- // }).toErrorDev('Did not expect server HTML to contain a
in ');
+ resolve();
+ await promise;
+ Scheduler.unstable_flushAll();
+ await null;
+ jest.runAllTimers();
- expect(container.innerHTML).toContain('
A');
- expect(container.innerHTML).not.toContain('
B');
- if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
- expect(ref.current).not.toBe(span);
- } else {
- expect(ref.current).toBe(span);
+ expect(container.innerHTML).toContain('
A');
+ expect(container.innerHTML).not.toContain('
B');
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
+ expect(ref.current).not.toBe(span);
+ } else {
+ expect(ref.current).toBe(span);
+ }
+ if (__DEV__) {
+ expect(mockError).toHaveBeenCalledWith(
+ 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
+ 'span',
+ 'div',
+ '\n' +
+ ' in Suspense (at **)\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ );
+ }
+ } finally {
+ console.error = originalConsoleError;
}
});
@@ -3179,9 +3227,14 @@ describe('ReactDOMServerPartialHydration', () => {
});
});
}).toErrorDev(
- 'Warning: An error occurred during hydration. ' +
- 'The server HTML was replaced with client content in
.',
- {withoutStack: true},
+ [
+ 'Warning: An error occurred during hydration. ' +
+ 'The server HTML was replaced with client content in
.',
+ 'Warning: Expected server HTML to contain a matching
in .\n' +
+ ' in span (at **)\n' +
+ ' in App (at **)',
+ ],
+ {withoutStack: 1},
);
expect(Scheduler).toHaveYielded([
'Log recoverable error: An error occurred during hydration. The server ' +
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 4a460d584a53d..ab2d05cea0aac 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -183,8 +183,7 @@ function deleteHydratableInstance(
}
}
-function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
- fiber.flags = (fiber.flags & ~Hydrating) | Placement;
+function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
if (__DEV__) {
switch (returnFiber.tag) {
case HostRoot: {
@@ -283,6 +282,10 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
}
}
}
+function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
+ fiber.flags = (fiber.flags & ~Hydrating) | Placement;
+ warnNonhydratedInstance(returnFiber, fiber);
+}
function tryHydrate(fiber, nextInstance) {
switch (fiber.tag) {
@@ -353,12 +356,10 @@ function shouldClientRenderOnMismatch(fiber: Fiber) {
);
}
-function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
- if (shouldClientRenderOnMismatch(fiber)) {
- throw new Error(
- 'An error occurred during hydration. The server HTML was replaced with client content',
- );
- }
+function throwOnHydrationMismatch(fiber: Fiber) {
+ throw new Error(
+ 'An error occurred during hydration. The server HTML was replaced with client content',
+ );
}
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
@@ -367,7 +368,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
- throwOnHydrationMismatchIfConcurrentMode(fiber);
+ if (shouldClientRenderOnMismatch(fiber)) {
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+ throwOnHydrationMismatch(fiber);
+ }
// Nothing to hydrate. Make it an insertion.
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
isHydrating = false;
@@ -376,7 +380,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
const firstAttemptedInstance = nextInstance;
if (!tryHydrate(fiber, nextInstance)) {
- throwOnHydrationMismatchIfConcurrentMode(fiber);
+ if (shouldClientRenderOnMismatch(fiber)) {
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+ throwOnHydrationMismatch(fiber);
+ }
// If we can't hydrate this instance let's try the next one.
// We use this as a heuristic. It's based on intuition and not data so it
// might be flawed or unnecessary.
@@ -565,7 +572,7 @@ function popHydrationState(fiber: Fiber): boolean {
if (nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
warnIfUnhydratedTailNodes(fiber);
- throwOnHydrationMismatchIfConcurrentMode(fiber);
+ throwOnHydrationMismatch(fiber);
} else {
while (nextInstance) {
deleteHydratableInstance(fiber, nextInstance);
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index 3ee040237829a..9ad186495d8e2 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -183,8 +183,7 @@ function deleteHydratableInstance(
}
}
-function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
- fiber.flags = (fiber.flags & ~Hydrating) | Placement;
+function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
if (__DEV__) {
switch (returnFiber.tag) {
case HostRoot: {
@@ -283,6 +282,10 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
}
}
}
+function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
+ fiber.flags = (fiber.flags & ~Hydrating) | Placement;
+ warnNonhydratedInstance(returnFiber, fiber);
+}
function tryHydrate(fiber, nextInstance) {
switch (fiber.tag) {
@@ -353,12 +356,10 @@ function shouldClientRenderOnMismatch(fiber: Fiber) {
);
}
-function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
- if (shouldClientRenderOnMismatch(fiber)) {
- throw new Error(
- 'An error occurred during hydration. The server HTML was replaced with client content',
- );
- }
+function throwOnHydrationMismatch(fiber: Fiber) {
+ throw new Error(
+ 'An error occurred during hydration. The server HTML was replaced with client content',
+ );
}
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
@@ -367,7 +368,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
- throwOnHydrationMismatchIfConcurrentMode(fiber);
+ if (shouldClientRenderOnMismatch(fiber)) {
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+ throwOnHydrationMismatch(fiber);
+ }
// Nothing to hydrate. Make it an insertion.
insertNonHydratedInstance((hydrationParentFiber: any), fiber);
isHydrating = false;
@@ -376,7 +380,10 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
const firstAttemptedInstance = nextInstance;
if (!tryHydrate(fiber, nextInstance)) {
- throwOnHydrationMismatchIfConcurrentMode(fiber);
+ if (shouldClientRenderOnMismatch(fiber)) {
+ warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+ throwOnHydrationMismatch(fiber);
+ }
// If we can't hydrate this instance let's try the next one.
// We use this as a heuristic. It's based on intuition and not data so it
// might be flawed or unnecessary.
@@ -565,7 +572,7 @@ function popHydrationState(fiber: Fiber): boolean {
if (nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
warnIfUnhydratedTailNodes(fiber);
- throwOnHydrationMismatchIfConcurrentMode(fiber);
+ throwOnHydrationMismatch(fiber);
} else {
while (nextInstance) {
deleteHydratableInstance(fiber, nextInstance);