diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 25ec3c590ec..46c1cdb0ce6 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -244,11 +244,13 @@ describe('ReactDOMServerPartialHydration', () => {
function App() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
@@ -264,7 +266,7 @@ describe('ReactDOMServerPartialHydration', () => {
]);
expect(container.innerHTML).toBe(
- 'Hello
Component
Component
Component
Component
',
+ 'Hello
Component
Component
Component
Component
',
);
suspend = true;
@@ -282,31 +284,46 @@ describe('ReactDOMServerPartialHydration', () => {
// Unchanged
expect(container.innerHTML).toBe(
- 'HelloComponent
Component
Component
Component
',
+ 'Hello
Component
Component
Component
Component
',
);
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',
- ]);
+ function whatWeExpect() {
+ expect(() => {
+ expect(Scheduler).toFlushAndYield([
+ // first pass, mismatches at end
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ // second pass as client render
+ 'Hello',
+ 'Component',
+ 'Component',
+ 'Component',
+ 'Component',
+ ]);
+ }).toErrorDev(
+ 'Warning: An error occurred during hydration. expected article got DIV. At: [object HTMLDivElement] Inside: [object HTMLDivElement]',
+ {withoutStack: true},
+ );
+ }
+
+ if (__DEV__) {
+ expect(whatWeExpect).toThrow(
+ 'Received 5 arguments for a message with 2 placeholders',
+ );
+ } else {
+ whatWeExpect();
+ }
// Client rendered - suspense comment nodes removed
expect(container.innerHTML).toBe(
- 'HelloComponent
Component
Component
Mismatch',
+ 'Hello
Component
Component
Component
Mismatch ',
);
});
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 64fd789b7a1..09ad788faf1 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -66,7 +66,11 @@ import {
enableCreateEventHandleAPI,
enableScopeAPI,
} from 'shared/ReactFeatureFlags';
-import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
+import {
+ HostComponent,
+ HostText,
+ SuspenseComponent,
+} from 'react-reconciler/src/ReactWorkTags';
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
@@ -1077,6 +1081,28 @@ export function errorHydratingContainer(parentContainer: Container): void {
}
}
+export function warnOnHydrationMismatch(
+ fiber: Fiber,
+ parentFiber: Fiber | null,
+ hydratableInstance: HydratableInstance | null,
+) {
+ if (__DEV__) {
+ // eslint-disable-next-line react-internal/warning-args
+ console.error(
+ 'An error occurred during hydration. expected %s got %s. At: ',
+ fiber.type,
+ hydratableInstance ? hydratableInstance.nodeName : '',
+ hydratableInstance,
+ 'Inside: ',
+ parentFiber
+ ? parentFiber.tag === SuspenseComponent
+ ? ''
+ : parentFiber.stateNode
+ : null,
+ );
+ }
+}
+
export function getInstanceFromNode(node: HTMLElement): null | Object {
return getClosestInstanceFromNode(node) || null;
}
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
index 1392cf8a260..eabb6c48d11 100644
--- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
@@ -55,3 +55,4 @@ export const didNotFindHydratableInstance = shim;
export const didNotFindHydratableTextInstance = shim;
export const didNotFindHydratableSuspenseInstance = shim;
export const errorHydratingContainer = shim;
+export const warnOnHydrationMismatch = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 5e915579a7c..aeddd4b1e64 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -61,6 +61,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
+ warnOnHydrationMismatch as hostWarnOnHydrationMismatch,
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.new';
@@ -324,7 +325,15 @@ function tryHydrate(fiber, nextInstance) {
}
}
-function throwOnHydrationMismatchIfConcurrentMode(fiber) {
+export function warnOnHydrationMismatch(fiber: Fiber) {
+ hostWarnOnHydrationMismatch(
+ fiber,
+ hydrationParentFiber,
+ nextHydratableInstance,
+ );
+}
+
+function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if ((fiber.mode & ConcurrentMode) !== NoMode) {
throw new Error(
'An error occurred during hydration. The server HTML was replaced with client content',
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index e7e08d5f3b8..d0197e1c34b 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -61,6 +61,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
+ warnOnHydrationMismatch as hostWarnOnHydrationMismatch,
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.old';
@@ -324,7 +325,15 @@ function tryHydrate(fiber, nextInstance) {
}
}
-function throwOnHydrationMismatchIfConcurrentMode(fiber) {
+export function warnOnHydrationMismatch(fiber: Fiber) {
+ hostWarnOnHydrationMismatch(
+ fiber,
+ hydrationParentFiber,
+ nextHydratableInstance,
+ );
+}
+
+function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if ((fiber.mode & ConcurrentMode) !== NoMode) {
throw new Error(
'An error occurred during hydration. The server HTML was replaced with client content',
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index cd9931687ba..622be7dcc42 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -79,7 +79,10 @@ import {
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane.new';
-import {getIsHydrating} from './ReactFiberHydrationContext.new';
+import {
+ warnOnHydrationMismatch,
+ getIsHydrating,
+} from './ReactFiberHydrationContext.new';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -499,6 +502,18 @@ function throwException(
// Set a flag to indicate that we should try rendering the normal
// children again, not the fallback.
suspenseBoundary.flags |= ForceClientRender;
+ if (__DEV__) {
+ if (
+ value &&
+ value.message &&
+ value.message.startsWith instanceof Function &&
+ (value.message.startsWith: any)(
+ 'An error occurred during hydration',
+ )
+ ) {
+ warnOnHydrationMismatch(sourceFiber);
+ }
+ }
}
markSuspenseBoundaryShouldCapture(
suspenseBoundary,
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index 8f6d18a48de..fc6ceed10bb 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -79,7 +79,10 @@ import {
mergeLanes,
pickArbitraryLane,
} from './ReactFiberLane.old';
-import {getIsHydrating} from './ReactFiberHydrationContext.old';
+import {
+ warnOnHydrationMismatch,
+ getIsHydrating,
+} from './ReactFiberHydrationContext.old';
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -499,6 +502,18 @@ function throwException(
// Set a flag to indicate that we should try rendering the normal
// children again, not the fallback.
suspenseBoundary.flags |= ForceClientRender;
+ if (__DEV__) {
+ if (
+ value &&
+ value.message &&
+ value.message.startsWith instanceof Function &&
+ (value.message.startsWith: any)(
+ 'An error occurred during hydration',
+ )
+ ) {
+ warnOnHydrationMismatch(sourceFiber);
+ }
+ }
}
markSuspenseBoundaryShouldCapture(
suspenseBoundary,
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 6535d8d3fde..f658bafa785 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -189,3 +189,4 @@ export const didNotFindHydratableTextInstance =
export const didNotFindHydratableSuspenseInstance =
$$$hostConfig.didNotFindHydratableSuspenseInstance;
export const errorHydratingContainer = $$$hostConfig.errorHydratingContainer;
+export const warnOnHydrationMismatch = $$$hostConfig.warnOnHydrationMismatch;