diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index f6bee806f3ac2..ca0745edc230c 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -2958,4 +2958,44 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
expect(ref.current.innerHTML).toBe('Hidden child');
});
+
+ function itHydratesWithoutMismatch(msg, App) {
+ it(msg + ' without mismatch', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const finalHTML = ReactDOMServer.renderToString();
+ container.innerHTML = finalHTML;
+
+ ReactDOM.hydrateRoot(container, );
+ Scheduler.unstable_flushAll();
+ });
+ }
+
+ itHydratesWithoutMismatch('can hydrate empty string ', function App() {
+ return (
+
+
Test
+ {'' &&
Test
}
+
Test
+
+ );
+ });
+
+ itHydratesWithoutMismatch('can hydrate empty string simple', function App() {
+ return '';
+ });
+ itHydratesWithoutMismatch('can hydrate empty string simple', function App() {
+ return (
+ <>
+ {''}
+ {'sup'}
+ >
+ );
+ });
+ itHydratesWithoutMismatch(
+ 'can hydrate empty string without mismatch simple 2',
+ function App() {
+ return {'' && false};
+ },
+ );
});
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 64fd789b7a1e8..8a605b678f005 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -692,14 +692,33 @@ export function canHydrateTextInstance(
instance: HydratableInstance,
text: string,
): null | TextInstance {
- if (text === '' || instance.nodeType !== TEXT_NODE) {
- // Empty strings are not parsed by HTML so there won't be a correct match here.
+ if (
+ (instance.textContent !== '' && text === '') ||
+ instance.nodeType !== TEXT_NODE
+ ) {
return null;
}
// This has now been refined to a text node.
return ((instance: any): TextInstance);
}
+export function insertMissingEmptyTextNode(
+ instance: null | HydratableInstance,
+ parent: null | HydratableInstance,
+): null | HydratableInstance {
+ const parentNode = instance ? instance.parentNode : parent;
+ if (parentNode) {
+ const textNode = document.createTextNode('');
+ if (instance) {
+ parentNode.insertBefore(textNode, instance);
+ } else {
+ parentNode.appendChild(textNode);
+ }
+ return (textNode: TextInstance);
+ }
+ return null;
+}
+
export function canHydrateSuspenseInstance(
instance: HydratableInstance,
): null | SuspenseInstance {
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
index 1392cf8a26083..409cac25cfcfb 100644
--- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
@@ -23,6 +23,7 @@ export type SuspenseInstance = mixed;
export const supportsHydration = false;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
+export const insertMissingEmptyTextNode = shim;
export const canHydrateSuspenseInstance = shim;
export const isSuspenseInstancePending = shim;
export const isSuspenseInstanceFallback = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 614c6c9d946ca..106fe5c5010ae 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -24,6 +24,7 @@ import {
HostComponent,
HostText,
HostRoot,
+ HostPortal,
SuspenseComponent,
} from './ReactWorkTags';
import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
@@ -61,6 +62,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
+ insertMissingEmptyTextNode,
} from './ReactFiberHostConfig';
import {
enableClientRenderFallbackOnHydrationMismatch,
@@ -327,6 +329,36 @@ function tryHydrate(fiber, nextInstance) {
}
}
+function tryHydrateEmptyTextNode(
+ fiber: Fiber,
+ nextInstance: null | HydratableInstance,
+ parentFiber: null | Fiber,
+) {
+ if (
+ nextInstance &&
+ canHydrateTextInstance(nextInstance, fiber.pendingProps)
+ ) {
+ return nextInstance;
+ } else {
+ if (!parentFiber) {
+ return null;
+ }
+ switch (parentFiber.tag) {
+ case HostRoot:
+ case HostPortal:
+ return insertMissingEmptyTextNode(
+ nextInstance,
+ parentFiber.stateNode.containerInfo,
+ );
+ case HostComponent:
+ return insertMissingEmptyTextNode(nextInstance, parentFiber.stateNode);
+ default:
+ // Recurse upwards to find parent host node for text node
+ return tryHydrateEmptyTextNode(fiber, nextInstance, parentFiber.return);
+ }
+ }
+}
+
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
@@ -342,6 +374,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
+ if (fiber.tag === HostText && fiber.pendingProps === '') {
+ nextHydratableInstance = tryHydrateEmptyTextNode(
+ fiber,
+ nextHydratableInstance,
+ hydrationParentFiber,
+ );
+ }
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
throwOnHydrationMismatchIfConcurrentMode(fiber);
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index b7bca8217d979..68a6ed146309b 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -24,6 +24,7 @@ import {
HostComponent,
HostText,
HostRoot,
+ HostPortal,
SuspenseComponent,
} from './ReactWorkTags';
import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
@@ -61,6 +62,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
+ insertMissingEmptyTextNode,
} from './ReactFiberHostConfig';
import {
enableClientRenderFallbackOnHydrationMismatch,
@@ -327,6 +329,36 @@ function tryHydrate(fiber, nextInstance) {
}
}
+function tryHydrateEmptyTextNode(
+ fiber: Fiber,
+ nextInstance: null | HydratableInstance,
+ parentFiber: null | Fiber,
+) {
+ if (
+ nextInstance &&
+ canHydrateTextInstance(nextInstance, fiber.pendingProps)
+ ) {
+ return nextInstance;
+ } else {
+ if (!parentFiber) {
+ return null;
+ }
+ switch (parentFiber.tag) {
+ case HostRoot:
+ case HostPortal:
+ return insertMissingEmptyTextNode(
+ nextInstance,
+ parentFiber.stateNode.containerInfo,
+ );
+ case HostComponent:
+ return insertMissingEmptyTextNode(nextInstance, parentFiber.stateNode);
+ default:
+ // Recurse upwards to find parent host node for text node
+ return tryHydrateEmptyTextNode(fiber, nextInstance, parentFiber.return);
+ }
+ }
+}
+
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
@@ -342,6 +374,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
}
+ if (fiber.tag === HostText && fiber.pendingProps === '') {
+ nextHydratableInstance = tryHydrateEmptyTextNode(
+ fiber,
+ nextHydratableInstance,
+ hydrationParentFiber,
+ );
+ }
let nextInstance = nextHydratableInstance;
if (!nextInstance) {
throwOnHydrationMismatchIfConcurrentMode(fiber);
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 6535d8d3fdec3..676664549d1f7 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -134,6 +134,8 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
// -------------------
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
+export const insertMissingEmptyTextNode =
+ $$$hostConfig.insertMissingEmptyTextNode;
export const canHydrateSuspenseInstance =
$$$hostConfig.canHydrateSuspenseInstance;
export const isSuspenseInstancePending =