Skip to content

Commit f898b8c

Browse files
committed
insert empty text node during hydration
1 parent 149b420 commit f898b8c

File tree

6 files changed

+142
-2
lines changed

6 files changed

+142
-2
lines changed

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2958,4 +2958,44 @@ describe('ReactDOMServerPartialHydration', () => {
29582958
expect(ref.current).toBe(span);
29592959
expect(ref.current.innerHTML).toBe('Hidden child');
29602960
});
2961+
2962+
function itHydratesWithoutMismatch(msg, App) {
2963+
it(msg + ' without mismatch', () => {
2964+
const container = document.createElement('div');
2965+
document.body.appendChild(container);
2966+
const finalHTML = ReactDOMServer.renderToString(<App />);
2967+
container.innerHTML = finalHTML;
2968+
2969+
ReactDOM.hydrateRoot(container, <App />);
2970+
Scheduler.unstable_flushAll();
2971+
});
2972+
}
2973+
2974+
itHydratesWithoutMismatch('can hydrate empty string ', function App() {
2975+
return (
2976+
<div>
2977+
<div id="test">Test</div>
2978+
{'' && <div>Test</div>}
2979+
<div>Test</div>
2980+
</div>
2981+
);
2982+
});
2983+
2984+
itHydratesWithoutMismatch('can hydrate empty string simple', function App() {
2985+
return '';
2986+
});
2987+
itHydratesWithoutMismatch('can hydrate empty string simple', function App() {
2988+
return (
2989+
<>
2990+
{''}
2991+
{'sup'}
2992+
</>
2993+
);
2994+
});
2995+
itHydratesWithoutMismatch(
2996+
'can hydrate empty string without mismatch simple 2',
2997+
function App() {
2998+
return <Suspense>{'' && false}</Suspense>;
2999+
},
3000+
);
29613001
});

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,14 +692,33 @@ export function canHydrateTextInstance(
692692
instance: HydratableInstance,
693693
text: string,
694694
): null | TextInstance {
695-
if (text === '' || instance.nodeType !== TEXT_NODE) {
696-
// Empty strings are not parsed by HTML so there won't be a correct match here.
695+
if (
696+
(instance.textContent !== '' && text === '') ||
697+
instance.nodeType !== TEXT_NODE
698+
) {
697699
return null;
698700
}
699701
// This has now been refined to a text node.
700702
return ((instance: any): TextInstance);
701703
}
702704

705+
export function insertMissingEmptyTextNode(
706+
instance: null | HydratableInstance,
707+
parent: null | HydratableInstance,
708+
): null | HydratableInstance {
709+
const parentNode = instance ? instance.parentNode : parent;
710+
if (parentNode) {
711+
const textNode = document.createTextNode('');
712+
if (instance) {
713+
parentNode.insertBefore(textNode, instance);
714+
} else {
715+
parentNode.appendChild(textNode);
716+
}
717+
return (textNode: TextInstance);
718+
}
719+
return null;
720+
}
721+
703722
export function canHydrateSuspenseInstance(
704723
instance: HydratableInstance,
705724
): null | SuspenseInstance {

packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type SuspenseInstance = mixed;
2323
export const supportsHydration = false;
2424
export const canHydrateInstance = shim;
2525
export const canHydrateTextInstance = shim;
26+
export const insertMissingEmptyTextNode = shim;
2627
export const canHydrateSuspenseInstance = shim;
2728
export const isSuspenseInstancePending = shim;
2829
export const isSuspenseInstanceFallback = shim;

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
HostComponent,
2525
HostText,
2626
HostRoot,
27+
HostPortal,
2728
SuspenseComponent,
2829
} from './ReactWorkTags';
2930
import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
@@ -61,6 +62,7 @@ import {
6162
didNotFindHydratableInstance,
6263
didNotFindHydratableTextInstance,
6364
didNotFindHydratableSuspenseInstance,
65+
insertMissingEmptyTextNode,
6466
} from './ReactFiberHostConfig';
6567
import {
6668
enableClientRenderFallbackOnHydrationMismatch,
@@ -327,6 +329,36 @@ function tryHydrate(fiber, nextInstance) {
327329
}
328330
}
329331

332+
function tryHydrateEmptyTextNode(
333+
fiber: Fiber,
334+
nextInstance: null | HydratableInstance,
335+
parentFiber: null | Fiber,
336+
) {
337+
if (
338+
nextInstance &&
339+
canHydrateTextInstance(nextInstance, fiber.pendingProps)
340+
) {
341+
return nextInstance;
342+
} else {
343+
if (!parentFiber) {
344+
return null;
345+
}
346+
switch (parentFiber.tag) {
347+
case HostRoot:
348+
case HostPortal:
349+
return insertMissingEmptyTextNode(
350+
nextInstance,
351+
parentFiber.stateNode.containerInfo,
352+
);
353+
case HostComponent:
354+
return insertMissingEmptyTextNode(nextInstance, parentFiber.stateNode);
355+
default:
356+
// Recurse upwards to find parent host node for text node
357+
return tryHydrateEmptyTextNode(fiber, nextInstance, parentFiber.return);
358+
}
359+
}
360+
}
361+
330362
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
331363
if (
332364
enableClientRenderFallbackOnHydrationMismatch &&
@@ -342,6 +374,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
342374
if (!isHydrating) {
343375
return;
344376
}
377+
if (fiber.tag === HostText && fiber.pendingProps === '') {
378+
nextHydratableInstance = tryHydrateEmptyTextNode(
379+
fiber,
380+
nextHydratableInstance,
381+
hydrationParentFiber,
382+
);
383+
}
345384
let nextInstance = nextHydratableInstance;
346385
if (!nextInstance) {
347386
throwOnHydrationMismatchIfConcurrentMode(fiber);

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
HostComponent,
2525
HostText,
2626
HostRoot,
27+
HostPortal,
2728
SuspenseComponent,
2829
} from './ReactWorkTags';
2930
import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
@@ -61,6 +62,7 @@ import {
6162
didNotFindHydratableInstance,
6263
didNotFindHydratableTextInstance,
6364
didNotFindHydratableSuspenseInstance,
65+
insertMissingEmptyTextNode,
6466
} from './ReactFiberHostConfig';
6567
import {
6668
enableClientRenderFallbackOnHydrationMismatch,
@@ -327,6 +329,36 @@ function tryHydrate(fiber, nextInstance) {
327329
}
328330
}
329331

332+
function tryHydrateEmptyTextNode(
333+
fiber: Fiber,
334+
nextInstance: null | HydratableInstance,
335+
parentFiber: null | Fiber,
336+
) {
337+
if (
338+
nextInstance &&
339+
canHydrateTextInstance(nextInstance, fiber.pendingProps)
340+
) {
341+
return nextInstance;
342+
} else {
343+
if (!parentFiber) {
344+
return null;
345+
}
346+
switch (parentFiber.tag) {
347+
case HostRoot:
348+
case HostPortal:
349+
return insertMissingEmptyTextNode(
350+
nextInstance,
351+
parentFiber.stateNode.containerInfo,
352+
);
353+
case HostComponent:
354+
return insertMissingEmptyTextNode(nextInstance, parentFiber.stateNode);
355+
default:
356+
// Recurse upwards to find parent host node for text node
357+
return tryHydrateEmptyTextNode(fiber, nextInstance, parentFiber.return);
358+
}
359+
}
360+
}
361+
330362
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
331363
if (
332364
enableClientRenderFallbackOnHydrationMismatch &&
@@ -342,6 +374,13 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
342374
if (!isHydrating) {
343375
return;
344376
}
377+
if (fiber.tag === HostText && fiber.pendingProps === '') {
378+
nextHydratableInstance = tryHydrateEmptyTextNode(
379+
fiber,
380+
nextHydratableInstance,
381+
hydrationParentFiber,
382+
);
383+
}
345384
let nextInstance = nextHydratableInstance;
346385
if (!nextInstance) {
347386
throwOnHydrationMismatchIfConcurrentMode(fiber);

packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
134134
// -------------------
135135
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
136136
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
137+
export const insertMissingEmptyTextNode =
138+
$$$hostConfig.insertMissingEmptyTextNode;
137139
export const canHydrateSuspenseInstance =
138140
$$$hostConfig.canHydrateSuspenseInstance;
139141
export const isSuspenseInstancePending =

0 commit comments

Comments
 (0)