Skip to content

Commit 101c086

Browse files
committed
hydrate singletons out of band
1 parent bcef5fa commit 101c086

15 files changed

+260
-151
lines changed

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

Lines changed: 43 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ import {
7474
HostResource,
7575
HostText,
7676
HostSingleton,
77-
HostPortal,
78-
HostRoot,
7977
} from 'react-reconciler/src/ReactWorkTags';
8078
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
8179

@@ -837,9 +835,25 @@ function getNextHydratable(node) {
837835
// Skip non-hydratable nodes.
838836
for (; node != null; node = ((node: any): Node).nextSibling) {
839837
const nodeType = node.nodeType;
840-
if (enableHostSingletons) {
838+
if (enableFloat) {
841839
if (nodeType === ELEMENT_NODE) {
842-
if (isHostResourceInstance(((node: any): Element))) {
840+
const element: Element = (node: any);
841+
if (isHostResourceInstance(element)) {
842+
continue;
843+
}
844+
if (enableHostSingletons) {
845+
if (isHostSingletonInstance(element)) {
846+
continue;
847+
}
848+
}
849+
break;
850+
} else if (nodeType === TEXT_NODE) {
851+
break;
852+
}
853+
} else if (enableHostSingletons) {
854+
if (nodeType === ELEMENT_NODE) {
855+
const element: Element = (node: any);
856+
if (isHostSingletonInstance(element)) {
843857
continue;
844858
}
845859
break;
@@ -1237,11 +1251,13 @@ export function acquireSingletonInstance(
12371251
props: Props,
12381252
rootContainerInstance: Container,
12391253
hostContext: HostContext,
1254+
validateDOMNestingDev: boolean,
12401255
): Instance {
12411256
if (__DEV__) {
1242-
// TODO: take namespace into account when validating.
1243-
const hostContextDev = ((hostContext: any): HostContextDev);
1244-
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
1257+
if (validateDOMNestingDev) {
1258+
const hostContextDev = ((hostContext: any): HostContextDev);
1259+
validateDOMNesting(type, null, hostContextDev.ancestorInfo);
1260+
}
12451261
}
12461262
const ownerDocument = getOwnerDocumentFromRootContainer(
12471263
rootContainerInstance,
@@ -1250,32 +1266,36 @@ export function acquireSingletonInstance(
12501266
// always be a documentElement. With normal html parsing this will always be the case but
12511267
// with pathological manipulation the document can end up in a state where no documentElement
12521268
// exists. We create it here if missing so we can treat it as an invariant.
1253-
// It is important to note that thi dom mutation and others in this function happen
1269+
// It is important to note that this dom mutation and others in this function happen
12541270
// in render rather than commit. This is tolerable because they only happen in degenerate cases
1255-
if (!ownerDocument.documentElement) {
1256-
ownerDocument.append(ownerDocument.createElement('html'));
1271+
let htmlElement = ownerDocument.documentElement;
1272+
if (!htmlElement) {
1273+
htmlElement = ownerDocument.appendChild(
1274+
ownerDocument.createElement('html'),
1275+
);
12571276
}
12581277
switch (type) {
12591278
case 'html': {
1260-
// We ensure this exists just above
1261-
return ((ownerDocument.documentElement: any): HTMLHtmlElement);
1279+
return htmlElement;
12621280
}
12631281
case 'head': {
1264-
if (!ownerDocument.head) {
1265-
ownerDocument.insertBefore(
1282+
let head = ownerDocument.head;
1283+
if (!head) {
1284+
head = htmlElement.insertBefore(
12661285
ownerDocument.createElement('head'),
12671286
ownerDocument.firstChild,
12681287
);
12691288
}
12701289
// We ensure this exists just above
1271-
return ((ownerDocument.head: any): HTMLHeadElement);
1290+
return head;
12721291
}
12731292
case 'body': {
1274-
if (!ownerDocument.body) {
1275-
ownerDocument.appendChild(ownerDocument.createElement('body'));
1293+
let body = ownerDocument.body;
1294+
if (!body) {
1295+
body = htmlElement.appendChild(ownerDocument.createElement('body'));
12761296
}
12771297
// We ensure this exists just above
1278-
return ((ownerDocument.body: any): HTMLBodyElement);
1298+
return body;
12791299
}
12801300
default: {
12811301
throw new Error(
@@ -1312,47 +1332,6 @@ export function resetSingletonInstance(
13121332
updateFiberProps(instance, props);
13131333
}
13141334

1315-
export function getInsertionEdge(parent: Instance): ?InstanceSibling {
1316-
if (enableHostSingletons) {
1317-
let node = null;
1318-
let nextNode = parent.lastChild;
1319-
let fallbackNode;
1320-
while (nextNode != null) {
1321-
const fiber = getInstanceFromNodeDOMTree(nextNode);
1322-
if (fiber) {
1323-
// We intentionally start with fiber rather than fiber.return because we want to
1324-
// account whether the node we found is the root itself. This comes into play
1325-
// when you portal into the same element that contains the HostRoot
1326-
let parentFiber = fiber;
1327-
while (parentFiber !== null) {
1328-
if (
1329-
(parentFiber.tag === HostComponent &&
1330-
parentFiber.stateNode === parent) ||
1331-
((parentFiber.tag === HostPortal || parentFiber.tag === HostRoot) &&
1332-
parentFiber.stateNode.containerInfo === parent)
1333-
) {
1334-
return node;
1335-
}
1336-
if (fallbackNode === undefined && parentFiber.tag === HostRoot) {
1337-
// When we find out first Fiber Node we capture the preceding Node to use as a fallback
1338-
// This is because we want to append to sibling fiber trees but prepend non-fiber trees
1339-
// If we don't end up finding an explicit insertion point based on existing siblings
1340-
fallbackNode = fallbackNode || node;
1341-
}
1342-
parentFiber = parentFiber.return;
1343-
}
1344-
}
1345-
1346-
node = nextNode;
1347-
nextNode = nextNode.previousSibling;
1348-
}
1349-
// We return the fallbackNode if we found one (the Node following the first React owned Node)
1350-
// Otherwise we return the first Node in the list of Siblings
1351-
return fallbackNode !== undefined ? fallbackNode : node;
1352-
}
1353-
return null;
1354-
}
1355-
13561335
export function clearSingletonInstance(instance: Instance) {
13571336
const tagName = instance.tagName.toLowerCase();
13581337
switch (tagName) {
@@ -1550,14 +1529,14 @@ export const supportsResources = true;
15501529
export {isHostResourceType};
15511530
function isHostResourceInstance(instance: Instance | Container): boolean {
15521531
if (instance.nodeType === ELEMENT_NODE) {
1553-
// $FlowFixMe[prop-missing] Flow doesn't understand `nodeType` test.
1554-
switch (instance.tagName.toLowerCase()) {
1532+
const element: Element = (instance: any);
1533+
switch (element.tagName.toLowerCase()) {
15551534
case 'link': {
1556-
const rel = ((instance: any): HTMLLinkElement).rel;
1535+
const linkEl: HTMLLinkElement = (element: any);
1536+
const rel = linkEl.rel;
15571537
return (
15581538
rel === 'preload' ||
1559-
// $FlowFixMe[prop-missing] Flow doesn't understand `nodeType` test.
1560-
(rel === 'stylesheet' && instance.hasAttribute('data-rprec'))
1539+
(rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
15611540
);
15621541
}
15631542
default: {

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -767,8 +767,60 @@ describe('ReactDOMFloat', () => {
767767
);
768768
});
769769

770-
// @gate enableFloat
770+
// @gate enableFloat && enableHostSingletons
771771
it('retains styles even when a new html, head, and/body mount', async () => {
772+
await actIntoEmptyDocument(() => {
773+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
774+
<html>
775+
<head />
776+
<body>
777+
<link rel="stylesheet" href="foo" precedence="foo" />
778+
<link rel="stylesheet" href="bar" precedence="bar" />
779+
server
780+
</body>
781+
</html>,
782+
);
783+
pipe(writable);
784+
});
785+
const errors = [];
786+
ReactDOMClient.hydrateRoot(
787+
document,
788+
<html>
789+
<head>
790+
<link rel="stylesheet" href="qux" precedence="qux" />
791+
<link rel="stylesheet" href="foo" precedence="foo" />
792+
</head>
793+
<body>client</body>
794+
</html>,
795+
{
796+
onRecoverableError(error) {
797+
errors.push(error.message);
798+
},
799+
},
800+
);
801+
expect(() => {
802+
expect(Scheduler).toFlushWithoutYielding();
803+
}).toErrorDev(
804+
[
805+
'Warning: Text content did not match. Server: "server" Client: "client"',
806+
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
807+
],
808+
{withoutStack: 1},
809+
);
810+
expect(getVisibleChildren(document)).toEqual(
811+
<html>
812+
<head>
813+
<link rel="stylesheet" href="foo" data-rprec="foo" />
814+
<link rel="stylesheet" href="bar" data-rprec="bar" />
815+
<link rel="stylesheet" href="qux" data-rprec="qux" />
816+
</head>
817+
<body>client</body>
818+
</html>,
819+
);
820+
});
821+
822+
// @gate enableFloat && !enableHostSingletons
823+
it('retains styles even when a new html, head, and/body mount - without HostSingleton', async () => {
772824
await actIntoEmptyDocument(() => {
773825
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
774826
<html>
@@ -818,17 +870,16 @@ describe('ReactDOMFloat', () => {
818870
);
819871
});
820872

821-
// Disabling for now since we are going to live with not having a restore step while we consider
822-
// HostSingletons or other solutions
823-
// @gate enableFloat
824-
xit('retains styles in head through head remounts', async () => {
873+
// @gate enableFloat && enableHostSingletons
874+
it('retains styles in head through head remounts', async () => {
825875
const root = ReactDOMClient.createRoot(document);
826876
root.render(
827877
<html>
828878
<head key={1} />
829879
<body>
830880
<link rel="stylesheet" href="foo" precedence="foo" />
831881
<link rel="stylesheet" href="bar" precedence="bar" />
882+
{null}
832883
hello
833884
</body>
834885
</html>,
@@ -849,17 +900,21 @@ describe('ReactDOMFloat', () => {
849900
<head key={2} />
850901
<body>
851902
<link rel="stylesheet" href="foo" precedence="foo" />
852-
<link rel="stylesheet" href="bar" precedence="bar" />
903+
{null}
904+
<link rel="stylesheet" href="baz" precedence="baz" />
853905
hello
854906
</body>
855907
</html>,
856908
);
857909
expect(Scheduler).toFlushWithoutYielding();
910+
// The reason we do not see preloads in the head is they are inserted synchronously
911+
// during render and then when the new singleton mounts it resets it's content, retaining only styles
858912
expect(getVisibleChildren(document)).toEqual(
859913
<html>
860914
<head>
861915
<link rel="stylesheet" href="foo" data-rprec="foo" />
862916
<link rel="stylesheet" href="bar" data-rprec="bar" />
917+
<link rel="stylesheet" href="baz" data-rprec="baz" />
863918
</head>
864919
<body>hello</body>
865920
</html>,

0 commit comments

Comments
 (0)