Skip to content

[rfc] Insert empty text node during hydration #22803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);
container.innerHTML = finalHTML;

ReactDOM.hydrateRoot(container, <App />);
Scheduler.unstable_flushAll();
});
}

itHydratesWithoutMismatch('can hydrate empty string ', function App() {
return (
<div>
<div id="test">Test</div>
{'' && <div>Test</div>}
<div>Test</div>
</div>
);
});

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 <Suspense>{'' && false}</Suspense>;
},
);
});
23 changes: 21 additions & 2 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 39 additions & 0 deletions packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
HostComponent,
HostText,
HostRoot,
HostPortal,
SuspenseComponent,
} from './ReactWorkTags';
import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
Expand Down Expand Up @@ -61,6 +62,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
insertMissingEmptyTextNode,
} from './ReactFiberHostConfig';
import {
enableClientRenderFallbackOnHydrationMismatch,
Expand Down Expand Up @@ -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 &&
Expand All @@ -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);
Expand Down
39 changes: 39 additions & 0 deletions packages/react-reconciler/src/ReactFiberHydrationContext.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
HostComponent,
HostText,
HostRoot,
HostPortal,
SuspenseComponent,
} from './ReactWorkTags';
import {ChildDeletion, Placement, Hydrating} from './ReactFiberFlags';
Expand Down Expand Up @@ -61,6 +62,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
insertMissingEmptyTextNode,
} from './ReactFiberHostConfig';
import {
enableClientRenderFallbackOnHydrationMismatch,
Expand Down Expand Up @@ -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 &&
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down