Implement HostSingleton Fiber type (#25426)
Josh Story committed
Oct 11, 2022 at 08:42 UTC
2cf4352e1c81a5b8c3528519a128c20e8e65531d
54 files changed
+2276
-193
packages/react-art/src/ReactARTHostConfig.js
+1
@@ -244,6 +244,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
244
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
245
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
246
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
247
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
248
249
export function appendInitialChild(parentInstance, child) {
250
if (typeof child === 'string') {
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+8
-2
@@ -73,6 +73,7 @@ import {
73
enableTrustedTypesIntegration,
74
enableCustomElementPropertySupport,
75
enableClientRenderFallbackOnTextMismatch,
76
+ enableHostSingletons,
77
} from 'shared/ReactFeatureFlags';
78
import {
79
mediaEventTypes,
@@ -312,12 +313,17 @@ function setInitialDOMProperties(
313
// textContent on a <textarea> will cause the placeholder to not
314
// show within the <textarea> until it has been focused and blurred again.
315
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
315
- const canSetTextContent = tag !== 'textarea' || nextProp !== '';
316
+ const canSetTextContent =
317
+ (!enableHostSingletons || tag !== 'body') &&
318
+ (tag !== 'textarea' || nextProp !== '');
319
if (canSetTextContent) {
320
setTextContent(domElement, nextProp);
321
}
322
} else if (typeof nextProp === 'number') {
320
- setTextContent(domElement, '' + nextProp);
323
+ const canSetTextContent = !enableHostSingletons || tag !== 'body';
324
+ if (canSetTextContent) {
325
+ setTextContent(domElement, '' + nextProp);
326
+ }
327
}
328
} else if (
329
propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+18
-9
@@ -25,6 +25,7 @@ import type {
25
import {
26
HostComponent,
27
HostResource,
28
+ HostSingleton,
29
HostText,
30
HostRoot,
31
SuspenseComponent,
@@ -32,7 +33,11 @@ import {
33
34
import {getParentSuspenseInstance} from './ReactDOMHostConfig';
35
35
-import {enableScopeAPI, enableFloat} from 'shared/ReactFeatureFlags';
36
+import {
37
+ enableScopeAPI,
38
+ enableFloat,
39
+ enableHostSingletons,
40
+} from 'shared/ReactFeatureFlags';
41
42
const randomKey = Math.random()
43
.toString(36)
@@ -169,12 +174,14 @@ export function getInstanceFromNode(node: Node): Fiber | null {
174
(node: any)[internalInstanceKey] ||
175
(node: any)[internalContainerInstanceKey];
176
if (inst) {
177
+ const tag = inst.tag;
178
if (
173
- inst.tag === HostComponent ||
174
- inst.tag === HostText ||
175
- inst.tag === SuspenseComponent ||
176
- inst.tag === HostRoot ||
177
- (enableFloat ? inst.tag === HostResource : false)
179
+ tag === HostComponent ||
180
+ tag === HostText ||
181
+ tag === SuspenseComponent ||
182
+ (enableFloat ? tag === HostResource : false) ||
183
+ (enableHostSingletons ? tag === HostSingleton : false) ||
184
+ tag === HostRoot
185
) {
186
return inst;
187
} else {
@@ -189,10 +196,12 @@ export function getInstanceFromNode(node: Node): Fiber | null {
196
* DOM node.
197
*/
198
export function getNodeFromInstance(inst: Fiber): Instance | TextInstance {
199
+ const tag = inst.tag;
200
if (
193
- inst.tag === HostComponent ||
194
- inst.tag === HostText ||
195
- (enableFloat ? inst.tag === HostResource : false)
201
+ tag === HostComponent ||
202
+ (enableFloat ? tag === HostResource : false) ||
203
+ (enableHostSingletons ? tag === HostSingleton : false) ||
204
+ tag === HostText
205
) {
206
// In Fiber this, is just the state node right now. We assume it will be
207
// a host component or host text.
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+301
-53
@@ -25,7 +25,8 @@ import {
25
getInstanceFromNode as getInstanceFromNodeDOMTree,
26
isContainerMarkedAsRoot,
27
} from './ReactDOMComponentTree';
28
-export {detachDeletedInstance} from './ReactDOMComponentTree';
28
+import {detachDeletedInstance} from './ReactDOMComponentTree';
29
+export {detachDeletedInstance};
30
import {hasRole} from './DOMAccessibilityRoles';
31
import {
32
createElement,
@@ -41,6 +42,7 @@ import {
42
warnForDeletedHydratableText,
43
warnForInsertedHydratedElement,
44
warnForInsertedHydratedText,
45
+ getOwnerDocumentFromRootContainer,
46
} from './ReactDOMComponent';
47
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
48
import setTextContent from './setTextContent';
@@ -66,11 +68,13 @@ import {
68
enableCreateEventHandleAPI,
69
enableScopeAPI,
70
enableFloat,
71
+ enableHostSingletons,
72
} from 'shared/ReactFeatureFlags';
73
import {
74
HostComponent,
75
HostResource,
76
HostText,
77
+ HostSingleton,
78
} from 'react-reconciler/src/ReactWorkTags';
79
import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
80
@@ -393,6 +397,33 @@ const localRequestAnimationFrame =
397
typeof requestAnimationFrame === 'function'
398
? requestAnimationFrame
399
: scheduleTimeout;
400
+
401
+export function getInstanceFromNode(node: HTMLElement): null | Object {
402
+ return getClosestInstanceFromNode(node) || null;
403
+}
404
+
405
+export function preparePortalMount(portalInstance: Instance): void {
406
+ listenToAllSupportedEvents(portalInstance);
407
+}
408
+
409
+export function prepareScopeUpdate(
410
+ scopeInstance: ReactScopeInstance,
411
+ internalInstanceHandle: Object,
412
+): void {
413
+ if (enableScopeAPI) {
414
+ precacheFiberNode(internalInstanceHandle, scopeInstance);
415
+ }
416
+}
417
+
418
+export function getInstanceFromScope(
419
+ scopeInstance: ReactScopeInstance,
420
+): null | Object {
421
+ if (enableScopeAPI) {
422
+ return getFiberFromScopeInstance(scopeInstance);
423
+ }
424
+ return null;
425
+}
426
+
427
// -------------------
428
// Microtasks
429
// -------------------
@@ -679,16 +710,84 @@ export function unhideTextInstance(
710
}
711
712
export function clearContainer(container: Container): void {
682
- if (container.nodeType === ELEMENT_NODE) {
683
- ((container: any): Element).textContent = '';
684
- } else if (container.nodeType === DOCUMENT_NODE) {
685
- if (container.documentElement) {
686
- // $FlowFixMe[incompatible-call]
687
- container.removeChild(container.documentElement);
713
+ if (enableHostSingletons) {
714
+ // We have refined the container to Element type
715
+ const nodeType = container.nodeType;
716
+ if (nodeType === DOCUMENT_NODE || nodeType === ELEMENT_NODE) {
717
+ switch (container.nodeName) {
718
+ case '#document':
719
+ case 'HTML':
720
+ case 'HEAD':
721
+ case 'BODY': {
722
+ let node = container.firstChild;
723
+ while (node) {
724
+ const nextNode = node.nextSibling;
725
+ const nodeName = node.nodeName;
726
+ switch (nodeName) {
727
+ case 'HTML':
728
+ case 'HEAD':
729
+ case 'BODY': {
730
+ clearContainer((node: any));
731
+ // If these singleton instances had previously been rendered with React they
732
+ // may still hold on to references to the previous fiber tree. We detatch them
733
+ // prospectiveyl to reset them to a baseline starting state since we cannot create
734
+ // new instances.
735
+ detachDeletedInstance((node: any));
736
+ break;
737
+ }
738
+ case 'STYLE': {
739
+ break;
740
+ }
741
+ case 'LINK': {
742
+ if (
743
+ ((node: any): HTMLLinkElement).rel.toLowerCase() ===
744
+ 'stylesheet'
745
+ ) {
746
+ break;
747
+ }
748
+ }
749
+ // eslint-disable-next-line no-fallthrough
750
+ default: {
751
+ container.removeChild(node);
752
+ }
753
+ }
754
+ node = nextNode;
755
+ }
756
+ return;
757
+ }
758
+ default: {
759
+ container.textContent = '';
760
+ }
761
+ }
762
+ }
763
+ } else {
764
+ if (container.nodeType === ELEMENT_NODE) {
765
+ // We have refined the container to Element type
766
+ const element: Element = (container: any);
767
+ element.textContent = '';
768
+ } else if (container.nodeType === DOCUMENT_NODE) {
769
+ // We have refined the container to Document type
770
+ const doc: Document = (container: any);
771
+ if (doc.documentElement) {
772
+ doc.removeChild(doc.documentElement);
773
+ }
774
}
775
}
776
}
777
778
+// Making this so we can eventually move all of the instance caching to the commit phase.
779
+// Currently this is only used to associate fiber and props to instances for hydrating
780
+// HostSingletons. The reason we need it here is we only want to make this binding on commit
781
+// because only one fiber can own the instance at a time and render can fail/restart
782
+export function bindInstance(
783
+ instance: Instance,
784
+ props: Props,
785
+ internalInstanceHandle: mixed,
786
+) {
787
+ precacheFiberNode((internalInstanceHandle: any), instance);
788
+ updateFiberProps(instance, props);
789
+}
790
+
791
// -------------------
792
// Hydration
793
// -------------------
@@ -781,9 +880,52 @@ function getNextHydratable(node) {
880
// Skip non-hydratable nodes.
881
for (; node != null; node = ((node: any): Node).nextSibling) {
882
const nodeType = node.nodeType;
784
- if (enableFloat) {
883
+ if (enableFloat && enableHostSingletons) {
884
+ if (nodeType === ELEMENT_NODE) {
885
+ const element: Element = (node: any);
886
+ switch (element.tagName) {
887
+ case 'LINK': {
888
+ const linkEl: HTMLLinkElement = (element: any);
889
+ const rel = linkEl.rel;
890
+ if (
891
+ rel === 'preload' ||
892
+ (rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
893
+ ) {
894
+ continue;
895
+ }
896
+ break;
897
+ }
898
+ case 'HTML':
899
+ case 'HEAD':
900
+ case 'BODY': {
901
+ continue;
902
+ }
903
+ }
904
+ break;
905
+ } else if (nodeType === TEXT_NODE) {
906
+ break;
907
+ }
908
+ } else if (enableFloat) {
909
if (nodeType === ELEMENT_NODE) {
786
- if (isHostResourceInstance(((node: any): Element))) {
910
+ const element: Element = (node: any);
911
+ if (element.tagName === 'LINK') {
912
+ const linkEl: HTMLLinkElement = (element: any);
913
+ const rel = linkEl.rel;
914
+ if (
915
+ rel === 'preload' ||
916
+ (rel === 'stylesheet' && linkEl.hasAttribute('data-rprec'))
917
+ ) {
918
+ continue;
919
+ }
920
+ }
921
+ break;
922
+ } else if (nodeType === TEXT_NODE) {
923
+ break;
924
+ }
925
+ } else if (enableHostSingletons) {
926
+ if (nodeType === ELEMENT_NODE) {
927
+ const tag: string = (node: any).tagName;
928
+ if (tag === 'HTML' || tag === 'HEAD' || tag === 'BODY') {
929
continue;
930
}
931
break;
@@ -970,6 +1112,8 @@ export function commitHydratedSuspenseInstance(
1112
retryIfBlockedOn(suspenseInstance);
1113
}
1114
1115
+// @TODO remove this function once float lands and hydrated tail nodes
1116
+// are controlled by HostSingleton fibers
1117
export function shouldDeleteUnhydratedTailInstances(
1118
parentType: string,
1119
): boolean {
@@ -1174,31 +1318,9 @@ export function errorHydratingContainer(parentContainer: Container): void {
1318
}
1319
}
1320
1177
-export function getInstanceFromNode(node: HTMLElement): null | Object {
1178
- return getClosestInstanceFromNode(node) || null;
1179
-}
1180
-
1181
-export function preparePortalMount(portalInstance: Instance): void {
1182
- listenToAllSupportedEvents(portalInstance);
1183
-}
1184
-
1185
-export function prepareScopeUpdate(
1186
- scopeInstance: ReactScopeInstance,
1187
- internalInstanceHandle: Object,
1188
-): void {
1189
- if (enableScopeAPI) {
1190
- precacheFiberNode(internalInstanceHandle, scopeInstance);
1191
- }
1192
-}
1193
-
1194
-export function getInstanceFromScope(
1195
- scopeInstance: ReactScopeInstance,
1196
-): null | Object {
1197
- if (enableScopeAPI) {
1198
- return getFiberFromScopeInstance(scopeInstance);
1199
- }
1200
- return null;
1201
-}
1321
+// -------------------
1322
+// Test Selectors
1323
+// -------------------
1324
1325
export const supportsTestSelectors = true;
1326
@@ -1236,6 +1358,7 @@ export function matchAccessibilityRole(node: Instance, role: string): boolean {
1358
export function getTextContent(fiber: Fiber): string | null {
1359
switch (fiber.tag) {
1360
case HostResource:
1361
+ case HostSingleton:
1362
case HostComponent:
1363
let textContent = '';
1364
const childNodes = fiber.stateNode.childNodes;
@@ -1353,25 +1476,6 @@ export function requestPostPaintCallback(callback: (time: number) => void) {
1476
export const supportsResources = true;
1477
1478
export {isHostResourceType};
1356
-function isHostResourceInstance(instance: Instance | Container): boolean {
1357
- if (instance.nodeType === ELEMENT_NODE) {
1358
- // $FlowFixMe[prop-missing] Flow doesn't understand `nodeType` test.
1359
- switch (instance.tagName.toLowerCase()) {
1360
- case 'link': {
1361
- const rel = ((instance: any): HTMLLinkElement).rel;
1362
- return (
1363
- rel === 'preload' ||
1364
- // $FlowFixMe[prop-missing] Flow doesn't understand `nodeType` test.
1365
- (rel === 'stylesheet' && instance.hasAttribute('data-rprec'))
1366
- );
1367
- }
1368
- default: {
1369
- return false;
1370
- }
1371
- }
1372
- }
1373
- return false;
1374
-}
1479
1480
export function prepareRendererToRender(rootContainer: Container) {
1481
if (enableFloat) {
@@ -1390,3 +1494,147 @@ export {
1494
acquireResource,
1495
releaseResource,
1496
} from './ReactDOMFloatClient';
1497
+
1498
+// -------------------
1499
+// Singletons
1500
+// -------------------
1501
+
1502
+export const supportsSingletons = true;
1503
+
1504
+export function isHostSingletonType(type: string): boolean {
1505
+ return type === 'html' || type === 'head' || type === 'body';
1506
+}
1507
+
1508
+export function resolveSingletonInstance(
1509
+ type: string,
1510
+ props: Props,
1511
+ rootContainerInstance: Container,
1512
+ hostContext: HostContext,
1513
+ validateDOMNestingDev: boolean,
1514
+): Instance {
1515
+ if (__DEV__) {
1516
+ if (validateDOMNestingDev) {
1517
+ const hostContextDev = ((hostContext: any): HostContextDev);
1518
+ validateDOMNesting(type, null, hostContextDev.ancestorInfo);
1519
+ }
1520
+ }
1521
+ const ownerDocument = getOwnerDocumentFromRootContainer(
1522
+ rootContainerInstance,
1523
+ );
1524
+ switch (type) {
1525
+ case 'html': {
1526
+ const documentElement = ownerDocument.documentElement;
1527
+ if (!documentElement) {
1528
+ throw new Error(
1529
+ 'React expected an <html> element (document.documentElement) to exist in the Document but one was' +
1530
+ ' not found. React never removes the documentElement for any Document it renders into so' +
1531
+ ' the cause is likely in some other script running on this page.',
1532
+ );
1533
+ }
1534
+ return documentElement;
1535
+ }
1536
+ case 'head': {
1537
+ const head = ownerDocument.head;
1538
+ if (!head) {
1539
+ throw new Error(
1540
+ 'React expected a <head> element (document.head) to exist in the Document but one was' +
1541
+ ' not found. React never removes the head for any Document it renders into so' +
1542
+ ' the cause is likely in some other script running on this page.',
1543
+ );
1544
+ }
1545
+ return head;
1546
+ }
1547
+ case 'body': {
1548
+ const body = ownerDocument.body;
1549
+ if (!body) {
1550
+ throw new Error(
1551
+ 'React expected a <body> element (document.body) to exist in the Document but one was' +
1552
+ ' not found. React never removes the body for any Document it renders into so' +
1553
+ ' the cause is likely in some other script running on this page.',
1554
+ );
1555
+ }
1556
+ return body;
1557
+ }
1558
+ default: {
1559
+ throw new Error(
1560
+ 'resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.',
1561
+ );
1562
+ }
1563
+ }
1564
+}
1565
+
1566
+export function acquireSingletonInstance(
1567
+ type: string,
1568
+ props: Props,
1569
+ instance: Instance,
1570
+ internalInstanceHandle: Object,
1571
+): void {
1572
+ if (__DEV__) {
1573
+ const currentInstanceHandle = getInstanceFromNodeDOMTree(instance);
1574
+ if (currentInstanceHandle) {
1575
+ const tagName = instance.tagName.toLowerCase();
1576
+ console.error(
1577
+ 'You are mounting a new %s component when a previous one has not first unmounted. It is an' +
1578
+ ' error to render more than one %s component at a time and attributes and children of these' +
1579
+ ' components will likely fail in unpredictable ways. Please only render a single instance of' +
1580
+ ' <%s> and if you need to mount a new one, ensure any previous ones have unmounted first.',
1581
+ tagName,
1582
+ tagName,
1583
+ tagName,
1584
+ );
1585
+ }
1586
+ switch (type) {
1587
+ case 'html':
1588
+ case 'head':
1589
+ case 'body': {
1590
+ break;
1591
+ }
1592
+ default: {
1593
+ console.error(
1594
+ 'acquireSingletonInstance was called with an element type that is not supported. This is a bug in React.',
1595
+ );
1596
+ }
1597
+ }
1598
+ }
1599
+
1600
+ const attributes = instance.attributes;
1601
+ while (attributes.length) {
1602
+ instance.removeAttributeNode(attributes[0]);
1603
+ }
1604
+
1605
+ setInitialProperties(instance, type, props);
1606
+ precacheFiberNode(internalInstanceHandle, instance);
1607
+ updateFiberProps(instance, props);
1608
+}
1609
+
1610
+export function releaseSingletonInstance(instance: Instance): void {
1611
+ const attributes = instance.attributes;
1612
+ while (attributes.length) {
1613
+ instance.removeAttributeNode(attributes[0]);
1614
+ }
1615
+ detachDeletedInstance(instance);
1616
+}
1617
+
1618
+export function clearSingleton(instance: Instance): void {
1619
+ const element: Element = (instance: any);
1620
+ let node = element.firstChild;
1621
+ while (node) {
1622
+ const nextNode = node.nextSibling;
1623
+ const nodeName = node.nodeName;
1624
+ if (getInstanceFromNodeDOMTree(node)) {
1625
+ // retain nodes owned by React
1626
+ } else if (
1627
+ nodeName === 'HEAD' ||
1628
+ nodeName === 'BODY' ||
1629
+ nodeName === 'STYLE' ||
1630
+ (nodeName === 'LINK' &&
1631
+ ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
1632
+ ) {
1633
+ // retain these nodes
1634
+ } else {
1635
+ element.removeChild(node);
1636
+ }
1637
+ node = nextNode;
1638
+ }
1639
+ return;
1640
+}
packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
+18
-5
@@ -34,6 +34,7 @@ import {
34
HostPortal,
35
HostComponent,
36
HostResource,
37
+ HostSingleton,
38
HostText,
39
ScopeComponent,
40
} from 'react-reconciler/src/ReactWorkTags';
@@ -54,6 +55,7 @@ import {
55
enableCreateEventHandleAPI,
56
enableScopeAPI,
57
enableFloat,
58
+ enableHostSingletons,
59
} from 'shared/ReactFeatureFlags';
60
import {
61
invokeGuardedCallbackAndCatchFirstError,
@@ -626,7 +628,8 @@ export function dispatchEventForPluginEventSystem(
628
if (
629
parentTag === HostComponent ||
630
parentTag === HostText ||
629
- (enableFloat ? parentTag === HostResource : false)
631
+ (enableFloat ? parentTag === HostResource : false) ||
632
+ (enableHostSingletons ? parentTag === HostSingleton : false)
633
) {
634
node = ancestorInst = parentNode;
635
continue mainLoop;
@@ -682,7 +685,9 @@ export function accumulateSinglePhaseListeners(
685
const {stateNode, tag} = instance;
686
// Handle listeners that are on HostComponents (i.e. <div>)
687
if (
685
- (tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
688
+ (tag === HostComponent ||
689
+ (enableFloat ? tag === HostResource : false) ||
690
+ (enableHostSingletons ? tag === HostSingleton : false)) &&
691
stateNode !== null
692
) {
693
lastHostComponent = stateNode;
@@ -796,7 +801,9 @@ export function accumulateTwoPhaseListeners(
801
const {stateNode, tag} = instance;
802
// Handle listeners that are on HostComponents (i.e. <div>)
803
if (
799
- (tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
804
+ (tag === HostComponent ||
805
+ (enableFloat ? tag === HostResource : false) ||
806
+ (enableHostSingletons ? tag === HostSingleton : false)) &&
807
stateNode !== null
808
) {
809
const currentTarget = stateNode;
@@ -830,7 +837,11 @@ function getParent(inst: Fiber | null): Fiber | null {
837
// events to their parent. We could also go through parentNode on the
838
// host node but that wouldn't work for React Native and doesn't let us
839
// do the portal feature.
833
- } while (inst && inst.tag !== HostComponent);
840
+ } while (
841
+ inst &&
842
+ inst.tag !== HostComponent &&
843
+ (!enableHostSingletons ? true : inst.tag !== HostSingleton)
844
+ );
845
if (inst) {
846
return inst;
847
}
@@ -897,7 +908,9 @@ function accumulateEnterLeaveListenersForEvent(
908
break;
909
}
910
if (
900
- (tag === HostComponent || (enableFloat ? tag === HostResource : false)) &&
911
+ (tag === HostComponent ||
912
+ (enableFloat ? tag === HostResource : false) ||
913
+ (enableHostSingletons ? tag === HostSingleton : false)) &&
914
stateNode !== null
915
) {
916
const currentTarget = stateNode;
packages/react-dom-bindings/src/events/plugins/EnterLeaveEventPlugin.js
+10
-2
@@ -24,7 +24,12 @@ import {
24
} from '../../client/ReactDOMComponentTree';
25
import {accumulateEnterLeaveTwoPhaseListeners} from '../DOMPluginEventSystem';
26
27
-import {HostComponent, HostText} from 'react-reconciler/src/ReactWorkTags';
27
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
28
+import {
29
+ HostComponent,
30
+ HostSingleton,
31
+ HostText,
32
+} from 'react-reconciler/src/ReactWorkTags';
33
import {getNearestMountedFiber} from 'react-reconciler/src/ReactFiberTreeReflection';
34
35
function registerEvents() {
@@ -102,9 +107,12 @@ function extractEvents(
107
to = related ? getClosestInstanceFromNode((related: any)) : null;
108
if (to !== null) {
109
const nearestMounted = getNearestMountedFiber(to);
110
+ const tag = to.tag;
111
if (
112
to !== nearestMounted ||
107
- (to.tag !== HostComponent && to.tag !== HostText)
113
+ (tag !== HostComponent &&
114
+ (!enableHostSingletons ? true : tag !== HostSingleton) &&
115
+ tag !== HostText)
116
) {
117
to = null;
118
}
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
-1
@@ -983,7 +983,6 @@ describe('ReactDOMFizzServer', () => {
983
});
984
985
// @gate enableSuspenseList
986
-
986
it('shows inserted items before pending in a SuspenseList as fallbacks while hydrating', async () => {
987
const ref = React.createRef();
988
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+61
-6
@@ -1028,8 +1028,60 @@ describe('ReactDOMFloat', () => {
1028
);
1029
});
1030
1031
- // @gate enableFloat
1031
+ // @gate enableFloat && enableHostSingletons
1032
it('retains styles even when a new html, head, and/body mount', async () => {
1033
+ await actIntoEmptyDocument(() => {
1034
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1035
+ <html>
1036
+ <head />
1037
+ <body>
1038
+ <link rel="stylesheet" href="foo" precedence="foo" />
1039
+ <link rel="stylesheet" href="bar" precedence="bar" />
1040
+ server
1041
+ </body>
1042
+ </html>,
1043
+ );
1044
+ pipe(writable);
1045
+ });
1046
+ const errors = [];
1047
+ ReactDOMClient.hydrateRoot(
1048
+ document,
1049
+ <html>
1050
+ <head>
1051
+ <link rel="stylesheet" href="qux" precedence="qux" />
1052
+ <link rel="stylesheet" href="foo" precedence="foo" />
1053
+ </head>
1054
+ <body>client</body>
1055
+ </html>,
1056
+ {
1057
+ onRecoverableError(error) {
1058
+ errors.push(error.message);
1059
+ },
1060
+ },
1061
+ );
1062
+ expect(() => {
1063
+ expect(Scheduler).toFlushWithoutYielding();
1064
+ }).toErrorDev(
1065
+ [
1066
+ 'Warning: Text content did not match. Server: "server" Client: "client"',
1067
+ 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
1068
+ ],
1069
+ {withoutStack: 1},
1070
+ );
1071
+ expect(getVisibleChildren(document)).toEqual(
1072
+ <html>
1073
+ <head>
1074
+ <link rel="stylesheet" href="foo" data-rprec="foo" />
1075
+ <link rel="stylesheet" href="bar" data-rprec="bar" />
1076
+ <link rel="stylesheet" href="qux" data-rprec="qux" />
1077
+ </head>
1078
+ <body>client</body>
1079
+ </html>,
1080
+ );
1081
+ });
1082
+
1083
+ // @gate enableFloat && !enableHostSingletons
1084
+ it('retains styles even when a new html, head, and/body mount - without HostSingleton', async () => {
1085
await actIntoEmptyDocument(() => {
1086
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
1087
<html>
@@ -1079,10 +1131,8 @@ describe('ReactDOMFloat', () => {
1131
);
1132
});
1133
1082
- // Disabling for now since we are going to live with not having a restore step while we consider
1083
- // HostSingletons or other solutions
1084
- // @gate enableFloat
1085
- xit('retains styles in head through head remounts', async () => {
1134
+ // @gate enableFloat && enableHostSingletons
1135
+ it('retains styles in head through head remounts', async () => {
1136
const root = ReactDOMClient.createRoot(document);
1137
root.render(
1138
<html>
@@ -1090,6 +1140,7 @@ describe('ReactDOMFloat', () => {
1140
<body>
1141
<link rel="stylesheet" href="foo" precedence="foo" />
1142
<link rel="stylesheet" href="bar" precedence="bar" />
1143
+ {null}
1144
hello
1145
</body>
1146
</html>,
@@ -1110,17 +1161,21 @@ describe('ReactDOMFloat', () => {
1161
<head key={2} />
1162
<body>
1163
<link rel="stylesheet" href="foo" precedence="foo" />
1113
- <link rel="stylesheet" href="bar" precedence="bar" />
1164
+ {null}
1165
+ <link rel="stylesheet" href="baz" precedence="baz" />
1166
hello
1167
</body>
1168
</html>,
1169
);
1170
expect(Scheduler).toFlushWithoutYielding();
1171
+ // The reason we do not see preloads in the head is they are inserted synchronously
1172
+ // during render and then when the new singleton mounts it resets it's content, retaining only styles
1173
expect(getVisibleChildren(document)).toEqual(
1174
<html>
1175
<head>
1176
<link rel="stylesheet" href="foo" data-rprec="foo" />
1177
<link rel="stylesheet" href="bar" data-rprec="bar" />
1178
+ <link rel="stylesheet" href="baz" data-rprec="baz" />
1179
</head>
1180
<body>hello</body>
1181
</html>,
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+14
-9
@@ -358,22 +358,27 @@ describe('ReactDOMRoot', () => {
358
);
359
});
360
361
- // @gate !__DEV__ || !enableFloat
361
it('warns if updating a root that has had its contents removed', async () => {
362
const root = ReactDOMClient.createRoot(container);
363
root.render(<div>Hi</div>);
364
Scheduler.unstable_flushAll();
365
container.innerHTML = '';
366
368
- expect(() => {
367
+ if (gate(flags => flags.enableFloat || flags.enableHostSingletons)) {
368
+ // When either of these flags are on this validation is turned off so we
369
+ // expect there to be no warnings
370
root.render(<div>Hi</div>);
370
- }).toErrorDev(
371
- 'render(...): It looks like the React-rendered content of the ' +
372
- 'root container was removed without using React. This is not ' +
373
- 'supported and will cause errors. Instead, call ' +
374
- "root.unmount() to empty a root's container.",
375
- {withoutStack: true},
376
- );
371
+ } else {
372
+ expect(() => {
373
+ root.render(<div>Hi</div>);
374
+ }).toErrorDev(
375
+ 'render(...): It looks like the React-rendered content of the ' +
376
+ 'root container was removed without using React. This is not ' +
377
+ 'supported and will cause errors. Instead, call ' +
378
+ "root.unmount() to empty a root's container.",
379
+ {withoutStack: true},
380
+ );
381
+ }
382
});
383
384
it('opts-in to concurrent default updates', async () => {
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
new
+982
@@ -0,0 +1,982 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let JSDOM;
13
+let Stream;
14
+let Scheduler;
15
+let React;
16
+let ReactDOMClient;
17
+let ReactDOMFizzServer;
18
+let document;
19
+let writable;
20
+let container;
21
+let buffer = '';
22
+let hasErrored = false;
23
+let fatalError = undefined;
24
+
25
+describe('ReactDOM HostSingleton', () => {
26
+ beforeEach(() => {
27
+ jest.resetModules();
28
+ JSDOM = require('jsdom').JSDOM;
29
+ Scheduler = require('scheduler');
30
+ React = require('react');
31
+ ReactDOMClient = require('react-dom/client');
32
+ ReactDOMFizzServer = require('react-dom/server');
33
+ Stream = require('stream');
34
+
35
+ // Test Environment
36
+ const jsdom = new JSDOM(
37
+ '<!DOCTYPE html><html><head></head><body><div id="container">',
38
+ {
39
+ runScripts: 'dangerously',
40
+ },
41
+ );
42
+ document = jsdom.window.document;
43
+ container = document.getElementById('container');
44
+
45
+ buffer = '';
46
+ hasErrored = false;
47
+
48
+ writable = new Stream.PassThrough();
49
+ writable.setEncoding('utf8');
50
+ writable.on('data', chunk => {
51
+ buffer += chunk;
52
+ });
53
+ writable.on('error', error => {
54
+ hasErrored = true;
55
+ fatalError = error;
56
+ });
57
+ });
58
+
59
+ async function actIntoEmptyDocument(callback) {
60
+ await callback();
61
+ // Await one turn around the event loop.
62
+ // This assumes that we'll flush everything we have so far.
63
+ await new Promise(resolve => {
64
+ setImmediate(resolve);
65
+ });
66
+ if (hasErrored) {
67
+ throw fatalError;
68
+ }
69
+
70
+ const bufferedContent = buffer;
71
+ buffer = '';
72
+
73
+ const jsdom = new JSDOM(bufferedContent, {
74
+ runScripts: 'dangerously',
75
+ });
76
+ document = jsdom.window.document;
77
+ container = document;
78
+ }
79
+
80
+ function getVisibleChildren(element) {
81
+ const children = [];
82
+ let node = element.firstChild;
83
+ while (node) {
84
+ if (node.nodeType === 1) {
85
+ if (
86
+ node.tagName !== 'SCRIPT' &&
87
+ node.tagName !== 'TEMPLATE' &&
88
+ node.tagName !== 'template' &&
89
+ !node.hasAttribute('hidden') &&
90
+ !node.hasAttribute('aria-hidden')
91
+ ) {
92
+ const props = {};
93
+ const attributes = node.attributes;
94
+ for (let i = 0; i < attributes.length; i++) {
95
+ if (
96
+ attributes[i].name === 'id' &&
97
+ attributes[i].value.includes(':')
98
+ ) {
99
+ // We assume this is a React added ID that's a non-visual implementation detail.
100
+ continue;
101
+ }
102
+ props[attributes[i].name] = attributes[i].value;
103
+ }
104
+ props.children = getVisibleChildren(node);
105
+ children.push(React.createElement(node.tagName.toLowerCase(), props));
106
+ }
107
+ } else if (node.nodeType === 3) {
108
+ children.push(node.data);
109
+ }
110
+ node = node.nextSibling;
111
+ }
112
+ return children.length === 0
113
+ ? undefined
114
+ : children.length === 1
115
+ ? children[0]
116
+ : children;
117
+ }
118
+
119
+ // @gate enableHostSingletons
120
+ it('warns if you render the same singleton twice at the same time', async () => {
121
+ const root = ReactDOMClient.createRoot(document);
122
+ root.render(
123
+ <html>
124
+ <head lang="en">
125
+ <title>Hello</title>
126
+ </head>
127
+ <body />
128
+ </html>,
129
+ );
130
+ expect(Scheduler).toFlushWithoutYielding();
131
+ expect(getVisibleChildren(document)).toEqual(
132
+ <html>
133
+ <head lang="en">
134
+ <title>Hello</title>
135
+ </head>
136
+ <body />
137
+ </html>,
138
+ );
139
+ root.render(
140
+ <html>
141
+ <head lang="en">
142
+ <title>Hello</title>
143
+ </head>
144
+ <head lang="es" data-foo="foo">
145
+ <title>Hola</title>
146
+ </head>
147
+ <body />
148
+ </html>,
149
+ );
150
+ expect(() => {
151
+ expect(Scheduler).toFlushWithoutYielding();
152
+ }).toErrorDev(
153
+ 'Warning: You are mounting a new head component when a previous one has not first unmounted. It is an error to render more than one head component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <head> and if you need to mount a new one, ensure any previous ones have unmounted first',
154
+ );
155
+ expect(getVisibleChildren(document)).toEqual(
156
+ <html>
157
+ <head lang="es" data-foo="foo">
158
+ <title>Hello</title>
159
+ <title>Hola</title>
160
+ </head>
161
+ <body />
162
+ </html>,
163
+ );
164
+
165
+ root.render(
166
+ <html>
167
+ {null}
168
+ {null}
169
+ <head lang="fr">
170
+ <title>Bonjour</title>
171
+ </head>
172
+ <body />
173
+ </html>,
174
+ );
175
+ expect(Scheduler).toFlushWithoutYielding();
176
+ expect(getVisibleChildren(document)).toEqual(
177
+ <html>
178
+ <head lang="fr">
179
+ <title>Bonjour</title>
180
+ </head>
181
+ <body />
182
+ </html>,
183
+ );
184
+
185
+ root.render(
186
+ <html>
187
+ <head lang="en">
188
+ <title>Hello</title>
189
+ </head>
190
+ <body />
191
+ </html>,
192
+ );
193
+ expect(Scheduler).toFlushWithoutYielding();
194
+ expect(getVisibleChildren(document)).toEqual(
195
+ <html>
196
+ <head lang="en">
197
+ <title>Hello</title>
198
+ </head>
199
+ <body />
200
+ </html>,
201
+ );
202
+ });
203
+
204
+ // @gate enableHostSingletons
205
+ it('renders into html, head, and body persistently so the node identities never change and extraneous styles are retained', async () => {
206
+ gate(flags => {
207
+ if (flags.enableHostSingletons !== true) {
208
+ // We throw here because when this test fails it ends up with sync work in a microtask
209
+ // that throws after the expectTestToFail check asserts the failure. this causes even the
210
+ // expected failure to fail. This just fails explicitly and early
211
+ throw new Error('manually opting out of test');
212
+ }
213
+ });
214
+ // Server render some html that will get replaced with a client render
215
+ await actIntoEmptyDocument(() => {
216
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
217
+ <html data-foo="foo">
218
+ <head data-bar="bar">
219
+ <link rel="stylesheet" href="resource" />
220
+ <title>a server title</title>
221
+ <link rel="stylesheet" href="3rdparty" />
222
+ <link rel="stylesheet" href="3rdparty2" />
223
+ </head>
224
+ <body data-baz="baz">
225
+ <div>hello world</div>
226
+ <style>
227
+ {`
228
+ body: {
229
+ background-color: red;
230
+ }`}
231
+ </style>
232
+ <div>goodbye</div>
233
+ </body>
234
+ </html>,
235
+ );
236
+ pipe(writable);
237
+ });
238
+ expect(getVisibleChildren(document)).toEqual(
239
+ <html data-foo="foo">
240
+ <head data-bar="bar">
241
+ <link rel="preload" href="resource" as="style" />
242
+ <link rel="preload" href="3rdparty" as="style" />
243
+ <link rel="preload" href="3rdparty2" as="style" />
244
+ <link rel="stylesheet" href="resource" />
245
+ <title>a server title</title>
246
+ <link rel="stylesheet" href="3rdparty" />
247
+ <link rel="stylesheet" href="3rdparty2" />
248
+ </head>
249
+ <body data-baz="baz">
250
+ <div>hello world</div>
251
+ <style>
252
+ {`
253
+ body: {
254
+ background-color: red;
255
+ }`}
256
+ </style>
257
+ <div>goodbye</div>
258
+ </body>
259
+ </html>,
260
+ );
261
+ const {documentElement, head, body} = document;
262
+ const persistentElements = [documentElement, head, body];
263
+
264
+ // Render into the document completely different html. Observe that styles
265
+ // are retained as are html, body, and head referential identities. Because this was
266
+ // server rendered and we are not hydrating we lose the semantic placement of the original
267
+ // head contents and everything gets preprended. In a future update we might emit an insertion
268
+ // edge from the server and make client rendering reslilient to interstitial placement
269
+ const root = ReactDOMClient.createRoot(document);
270
+ root.render(
271
+ <html data-client-foo="foo">
272
+ <head>
273
+ <title>a client title</title>
274
+ </head>
275
+ <body data-client-baz="baz">
276
+ <div>hello client</div>
277
+ </body>
278
+ </html>,
279
+ );
280
+ expect(Scheduler).toFlushWithoutYielding();
281
+ expect(persistentElements).toEqual([
282
+ document.documentElement,
283
+ document.head,
284
+ document.body,
285
+ ]);
286
+ // Similar to Hydration we don't reset attributes on the instance itself even on a fresh render.
287
+ expect(getVisibleChildren(document)).toEqual(
288
+ <html data-client-foo="foo">
289
+ <head>
290
+ <link rel="stylesheet" href="resource" />
291
+ <link rel="stylesheet" href="3rdparty" />
292
+ <link rel="stylesheet" href="3rdparty2" />
293
+ <title>a client title</title>
294
+ </head>
295
+ <body data-client-baz="baz">
296
+ <style>
297
+ {`
298
+ body: {
299
+ background-color: red;
300
+ }`}
301
+ </style>
302
+ <div>hello client</div>
303
+ </body>
304
+ </html>,
305
+ );
306
+
307
+ // Render new children and assert they append in the correct locations
308
+ root.render(
309
+ <html data-client-foo="foo">
310
+ <head>
311
+ <title>a client title</title>
312
+ <meta />
313
+ </head>
314
+ <body data-client-baz="baz">
315
+ <p>hello client again</p>
316
+ <div>hello client</div>
317
+ </body>
318
+ </html>,
319
+ );
320
+ expect(Scheduler).toFlushWithoutYielding();
321
+ expect(persistentElements).toEqual([
322
+ document.documentElement,
323
+ document.head,
324
+ document.body,
325
+ ]);
326
+ expect(getVisibleChildren(document)).toEqual(
327
+ <html data-client-foo="foo">
328
+ <head>
329
+ <link rel="stylesheet" href="resource" />
330
+ <link rel="stylesheet" href="3rdparty" />
331
+ <link rel="stylesheet" href="3rdparty2" />
332
+ <title>a client title</title>
333
+ <meta />
334
+ </head>
335
+ <body data-client-baz="baz">
336
+ <style>
337
+ {`
338
+ body: {
339
+ background-color: red;
340
+ }`}
341
+ </style>
342
+ <p>hello client again</p>
343
+ <div>hello client</div>
344
+ </body>
345
+ </html>,
346
+ );
347
+
348
+ // Remove some children
349
+ root.render(
350
+ <html data-client-foo="foo">
351
+ <head>
352
+ <title>a client title</title>
353
+ </head>
354
+ <body data-client-baz="baz">
355
+ <p>hello client again</p>
356
+ </body>
357
+ </html>,
358
+ );
359
+ expect(Scheduler).toFlushWithoutYielding();
360
+ expect(persistentElements).toEqual([
361
+ document.documentElement,
362
+ document.head,
363
+ document.body,
364
+ ]);
365
+ expect(getVisibleChildren(document)).toEqual(
366
+ <html data-client-foo="foo">
367
+ <head>
368
+ <link rel="stylesheet" href="resource" />
369
+ <link rel="stylesheet" href="3rdparty" />
370
+ <link rel="stylesheet" href="3rdparty2" />
371
+ <title>a client title</title>
372
+ </head>
373
+ <body data-client-baz="baz">
374
+ <style>
375
+ {`
376
+ body: {
377
+ background-color: red;
378
+ }`}
379
+ </style>
380
+ <p>hello client again</p>
381
+ </body>
382
+ </html>,
383
+ );
384
+
385
+ // Remove a persistent component
386
+ // @TODO figure out whether to clean up attributes. restoring them is likely
387
+ // not possible.
388
+ root.render(
389
+ <html data-client-foo="foo">
390
+ <head>
391
+ <title>a client title</title>
392
+ </head>
393
+ </html>,
394
+ );
395
+ expect(Scheduler).toFlushWithoutYielding();
396
+ expect(persistentElements).toEqual([
397
+ document.documentElement,
398
+ document.head,
399
+ document.body,
400
+ ]);
401
+ expect(getVisibleChildren(document)).toEqual(
402
+ <html data-client-foo="foo">
403
+ <head>
404
+ <link rel="stylesheet" href="resource" />
405
+ <link rel="stylesheet" href="3rdparty" />
406
+ <link rel="stylesheet" href="3rdparty2" />
407
+ <title>a client title</title>
408
+ </head>
409
+ <body>
410
+ <style>
411
+ {`
412
+ body: {
413
+ background-color: red;
414
+ }`}
415
+ </style>
416
+ </body>
417
+ </html>,
418
+ );
419
+
420
+ // unmount the root
421
+ root.unmount();
422
+ expect(Scheduler).toFlushWithoutYielding();
423
+ expect(persistentElements).toEqual([
424
+ document.documentElement,
425
+ document.head,
426
+ document.body,
427
+ ]);
428
+ expect(getVisibleChildren(document)).toEqual(
429
+ <html>
430
+ <head>
431
+ <link rel="stylesheet" href="resource" />
432
+ <link rel="stylesheet" href="3rdparty" />
433
+ <link rel="stylesheet" href="3rdparty2" />
434
+ </head>
435
+ <body>
436
+ <style>
437
+ {`
438
+ body: {
439
+ background-color: red;
440
+ }`}
441
+ </style>
442
+ </body>
443
+ </html>,
444
+ );
445
+
446
+ // Now let's hydrate the document with known mismatching content
447
+ // We assert that the identities of html, head, and body still haven't changed
448
+ // and that the embedded styles are still retained
449
+ const hydrationErrors = [];
450
+ let hydrateRoot = ReactDOMClient.hydrateRoot(
451
+ document,
452
+ <html data-client-foo="foo">
453
+ <head>
454
+ <title>a client title</title>
455
+ </head>
456
+ <body data-client-baz="baz">
457
+ <div>hello client</div>
458
+ </body>
459
+ </html>,
460
+ {
461
+ onRecoverableError(error, errorInfo) {
462
+ hydrationErrors.push([
463
+ error.message,
464
+ errorInfo.componentStack
465
+ ? errorInfo.componentStack.split('\n')[1].trim()
466
+ : null,
467
+ ]);
468
+ },
469
+ },
470
+ );
471
+ expect(() => {
472
+ expect(Scheduler).toFlushWithoutYielding();
473
+ }).toErrorDev(
474
+ [
475
+ `Warning: Expected server HTML to contain a matching <title> in <head>.
476
+ in title (at **)
477
+ in head (at **)
478
+ in html (at **)`,
479
+ `Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.`,
480
+ ],
481
+ {withoutStack: 1},
482
+ );
483
+ expect(hydrationErrors).toEqual([
484
+ [
485
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
486
+ 'at title',
487
+ ],
488
+ [
489
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
490
+ 'at div',
491
+ ],
492
+ [
493
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
494
+ null,
495
+ ],
496
+ ]);
497
+ expect(persistentElements).toEqual([
498
+ document.documentElement,
499
+ document.head,
500
+ document.body,
501
+ ]);
502
+ expect(getVisibleChildren(document)).toEqual(
503
+ <html data-client-foo="foo">
504
+ <head>
505
+ <link rel="stylesheet" href="resource" />
506
+ <link rel="stylesheet" href="3rdparty" />
507
+ <link rel="stylesheet" href="3rdparty2" />
508
+ <title>a client title</title>
509
+ </head>
510
+ <body data-client-baz="baz">
511
+ <style>
512
+ {`
513
+ body: {
514
+ background-color: red;
515
+ }`}
516
+ </style>
517
+ <div>hello client</div>
518
+ </body>
519
+ </html>,
520
+ );
521
+
522
+ // Reset the tree
523
+ hydrationErrors.length = 0;
524
+ hydrateRoot.unmount();
525
+
526
+ // Now we try hydrating again with matching nodes and we ensure
527
+ // the retained styles are bound to the hydrated fibers
528
+ const link = document.querySelector('link[rel="stylesheet"]');
529
+ const style = document.querySelector('style');
530
+ hydrateRoot = ReactDOMClient.hydrateRoot(
531
+ document,
532
+ <html data-client-foo="foo">
533
+ <head>
534
+ <link rel="stylesheet" href="resource" />
535
+ <link rel="stylesheet" href="3rdparty" />
536
+ <link rel="stylesheet" href="3rdparty2" />
537
+ </head>
538
+ <body data-client-baz="baz">
539
+ <style>
540
+ {`
541
+ body: {
542
+ background-color: red;
543
+ }`}
544
+ </style>
545
+ </body>
546
+ </html>,
547
+ {
548
+ onRecoverableError(error, errorInfo) {
549
+ hydrationErrors.push([
550
+ error.message,
551
+ errorInfo.componentStack
552
+ ? errorInfo.componentStack.split('\n')[1].trim()
553
+ : null,
554
+ ]);
555
+ },
556
+ },
557
+ );
558
+ expect(hydrationErrors).toEqual([]);
559
+ expect(Scheduler).toFlushWithoutYielding();
560
+ expect(persistentElements).toEqual([
561
+ document.documentElement,
562
+ document.head,
563
+ document.body,
564
+ ]);
565
+ expect([link, style]).toEqual([
566
+ document.querySelector('link[rel="stylesheet"]'),
567
+ document.querySelector('style'),
568
+ ]);
569
+ expect(getVisibleChildren(document)).toEqual(
570
+ <html>
571
+ <head>
572
+ <link rel="stylesheet" href="resource" />
573
+ <link rel="stylesheet" href="3rdparty" />
574
+ <link rel="stylesheet" href="3rdparty2" />
575
+ </head>
576
+ <body>
577
+ <style>
578
+ {`
579
+ body: {
580
+ background-color: red;
581
+ }`}
582
+ </style>
583
+ </body>
584
+ </html>,
585
+ );
586
+
587
+ // We unmount a final time and observe that still we retain our persistent nodes
588
+ // but they style contents which matched in hydration is removed
589
+ hydrateRoot.unmount();
590
+ expect(persistentElements).toEqual([
591
+ document.documentElement,
592
+ document.head,
593
+ document.body,
594
+ ]);
595
+ expect(getVisibleChildren(document)).toEqual(
596
+ <html>
597
+ <head />
598
+ <body />
599
+ </html>,
600
+ );
601
+ });
602
+
603
+ // This test is not supported in this implementation. If we reintroduce insertion edge we should revisit
604
+ // @gate enableHostSingletons
605
+ xit('is able to maintain insertions in head and body between tree-adjacent Nodes', async () => {
606
+ // Server render some html and hydrate on the client
607
+ await actIntoEmptyDocument(() => {
608
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
609
+ <html>
610
+ <head>
611
+ <title>title</title>
612
+ </head>
613
+ <body>
614
+ <div>hello</div>
615
+ </body>
616
+ </html>,
617
+ );
618
+ pipe(writable);
619
+ });
620
+ const root = ReactDOMClient.hydrateRoot(
621
+ document,
622
+ <html>
623
+ <head>
624
+ <title>title</title>
625
+ </head>
626
+ <body>
627
+ <div>hello</div>
628
+ </body>
629
+ </html>,
630
+ );
631
+ expect(Scheduler).toFlushWithoutYielding();
632
+
633
+ // We construct and insert some artificial stylesheets mimicing what a 3rd party script might do
634
+ // In the future we could hydrate with these already in the document but the rules are restrictive
635
+ // still so it would fail and fall back to client rendering
636
+ const [a, b, c, d, e, f, g, h] = 'abcdefgh'.split('').map(letter => {
637
+ const link = document.createElement('link');
638
+ link.rel = 'stylesheet';
639
+ link.href = letter;
640
+ return link;
641
+ });
642
+
643
+ const head = document.head;
644
+ const title = head.firstChild;
645
+ head.insertBefore(a, title);
646
+ head.insertBefore(b, title);
647
+ head.appendChild(c);
648
+ head.appendChild(d);
649
+
650
+ const bodyContent = document.body.firstChild;
651
+ const body = document.body;
652
+ body.insertBefore(e, bodyContent);
653
+ body.insertBefore(f, bodyContent);
654
+ body.appendChild(g);
655
+ body.appendChild(h);
656
+
657
+ expect(getVisibleChildren(document)).toEqual(
658
+ <html>
659
+ <head>
660
+ <link rel="stylesheet" href="a" />
661
+ <link rel="stylesheet" href="b" />
662
+ <title>title</title>
663
+ <link rel="stylesheet" href="c" />
664
+ <link rel="stylesheet" href="d" />
665
+ </head>
666
+ <body>
667
+ <link rel="stylesheet" href="e" />
668
+ <link rel="stylesheet" href="f" />
669
+ <div>hello</div>
670
+ <link rel="stylesheet" href="g" />
671
+ <link rel="stylesheet" href="h" />
672
+ </body>
673
+ </html>,
674
+ );
675
+
676
+ // Unmount head and change children of body
677
+ root.render(
678
+ <html>
679
+ {null}
680
+ <body>
681
+ <div>hello</div>
682
+ <div>world</div>
683
+ </body>
684
+ </html>,
685
+ );
686
+
687
+ expect(Scheduler).toFlushWithoutYielding();
688
+ expect(getVisibleChildren(document)).toEqual(
689
+ <html>
690
+ <head>
691
+ <link rel="stylesheet" href="a" />
692
+ <link rel="stylesheet" href="b" />
693
+ <link rel="stylesheet" href="c" />
694
+ <link rel="stylesheet" href="d" />
695
+ </head>
696
+ <body>
697
+ <link rel="stylesheet" href="e" />
698
+ <link rel="stylesheet" href="f" />
699
+ <div>hello</div>
700
+ <div>world</div>
701
+ <link rel="stylesheet" href="g" />
702
+ <link rel="stylesheet" href="h" />
703
+ </body>
704
+ </html>,
705
+ );
706
+
707
+ // Mount new head and unmount body
708
+ root.render(
709
+ <html>
710
+ <head>
711
+ <title>a new title</title>
712
+ </head>
713
+ </html>,
714
+ );
715
+ expect(Scheduler).toFlushWithoutYielding();
716
+ expect(getVisibleChildren(document)).toEqual(
717
+ <html>
718
+ <head>
719
+ <title>a new title</title>
720
+ <link rel="stylesheet" href="a" />
721
+ <link rel="stylesheet" href="b" />
722
+ <link rel="stylesheet" href="c" />
723
+ <link rel="stylesheet" href="d" />
724
+ </head>
725
+ <body>
726
+ <link rel="stylesheet" href="e" />
727
+ <link rel="stylesheet" href="f" />
728
+ <link rel="stylesheet" href="g" />
729
+ <link rel="stylesheet" href="h" />
730
+ </body>
731
+ </html>,
732
+ );
733
+ });
734
+
735
+ // @gate enableHostSingletons
736
+ it('clears persistent head and body when html is the container', async () => {
737
+ await actIntoEmptyDocument(() => {
738
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
739
+ <html>
740
+ <head>
741
+ <link rel="stylesheet" href="headbefore" />
742
+ <title>this should be removed</title>
743
+ <link rel="stylesheet" href="headafter" />
744
+ </head>
745
+ <body>
746
+ <link rel="stylesheet" href="bodybefore" />
747
+ <div>this should be removed</div>
748
+ <link rel="stylesheet" href="bodyafter" />
749
+ </body>
750
+ </html>,
751
+ );
752
+ pipe(writable);
753
+ });
754
+ container = document.documentElement;
755
+
756
+ const root = ReactDOMClient.createRoot(container);
757
+ root.render(
758
+ <>
759
+ <head>
760
+ <title>something new</title>
761
+ </head>
762
+ <body>
763
+ <div>something new</div>
764
+ </body>
765
+ </>,
766
+ );
767
+ expect(Scheduler).toFlushWithoutYielding();
768
+ expect(getVisibleChildren(document)).toEqual(
769
+ <html>
770
+ <head>
771
+ <link rel="stylesheet" href="headbefore" />
772
+ <link rel="stylesheet" href="headafter" />
773
+ <title>something new</title>
774
+ </head>
775
+ <body>
776
+ <link rel="stylesheet" href="bodybefore" />
777
+ <link rel="stylesheet" href="bodyafter" />
778
+ <div>something new</div>
779
+ </body>
780
+ </html>,
781
+ );
782
+ });
783
+
784
+ // @gate enableHostSingletons
785
+ it('clears persistent head when it is the container', async () => {
786
+ await actIntoEmptyDocument(() => {
787
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
788
+ <html>
789
+ <head>
790
+ <link rel="stylesheet" href="before" />
791
+ <title>this should be removed</title>
792
+ <link rel="stylesheet" href="after" />
793
+ </head>
794
+ <body />
795
+ </html>,
796
+ );
797
+ pipe(writable);
798
+ });
799
+ container = document.head;
800
+
801
+ const root = ReactDOMClient.createRoot(container);
802
+ root.render(<title>something new</title>);
803
+ expect(Scheduler).toFlushWithoutYielding();
804
+ expect(getVisibleChildren(document)).toEqual(
805
+ <html>
806
+ <head>
807
+ <link rel="stylesheet" href="before" />
808
+ <link rel="stylesheet" href="after" />
809
+ <title>something new</title>
810
+ </head>
811
+ <body />
812
+ </html>,
813
+ );
814
+ });
815
+
816
+ // @gate enableHostSingletons && enableFloat
817
+ it('clears persistent body when it is the container', async () => {
818
+ await actIntoEmptyDocument(() => {
819
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
820
+ <html>
821
+ <head />
822
+ <body>
823
+ <link rel="stylesheet" href="before" />
824
+ <div>this should be removed</div>
825
+ <link rel="stylesheet" href="after" />
826
+ </body>
827
+ </html>,
828
+ );
829
+ pipe(writable);
830
+ });
831
+ container = document.body;
832
+
833
+ let root;
834
+ // Given our new capabilities to render "safely" into the body we should consider removing this warning
835
+ expect(() => {
836
+ root = ReactDOMClient.createRoot(container);
837
+ }).toErrorDev(
838
+ 'Warning: createRoot(): Creating roots directly with document.body is discouraged, since its children are often manipulated by third-party scripts and browser extensions. This may lead to subtle reconciliation issues. Try using a container element created for your app.',
839
+ {withoutStack: true},
840
+ );
841
+ root.render(<div>something new</div>);
842
+ expect(Scheduler).toFlushWithoutYielding();
843
+ expect(getVisibleChildren(document)).toEqual(
844
+ <html>
845
+ <head>
846
+ <link rel="preload" as="style" href="before" />
847
+ <link rel="preload" as="style" href="after" />
848
+ </head>
849
+ <body>
850
+ <link rel="stylesheet" href="before" />
851
+ <link rel="stylesheet" href="after" />
852
+ <div>something new</div>
853
+ </body>
854
+ </html>,
855
+ );
856
+ });
857
+
858
+ it('renders single Text children into HostSingletons correctly', async () => {
859
+ await actIntoEmptyDocument(() => {
860
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
861
+ <html>
862
+ <head />
863
+ <body>foo</body>
864
+ </html>,
865
+ );
866
+ pipe(writable);
867
+ });
868
+
869
+ let root = ReactDOMClient.hydrateRoot(
870
+ document,
871
+ <html>
872
+ <head />
873
+ <body>foo</body>
874
+ </html>,
875
+ );
876
+ expect(Scheduler).toFlushWithoutYielding();
877
+ expect(getVisibleChildren(document)).toEqual(
878
+ <html>
879
+ <head />
880
+ <body>foo</body>
881
+ </html>,
882
+ );
883
+
884
+ root.render(
885
+ <html>
886
+ <head />
887
+ <body>bar</body>
888
+ </html>,
889
+ );
890
+ expect(Scheduler).toFlushWithoutYielding();
891
+ expect(getVisibleChildren(document)).toEqual(
892
+ <html>
893
+ <head />
894
+ <body>bar</body>
895
+ </html>,
896
+ );
897
+
898
+ root.unmount();
899
+
900
+ root = ReactDOMClient.createRoot(document);
901
+ root.render(
902
+ <html>
903
+ <head />
904
+ <body>baz</body>
905
+ </html>,
906
+ );
907
+ expect(Scheduler).toFlushWithoutYielding();
908
+ expect(getVisibleChildren(document)).toEqual(
909
+ <html>
910
+ <head />
911
+ <body>baz</body>
912
+ </html>,
913
+ );
914
+ });
915
+
916
+ it('supports going from single text child to many children back to single text child in body', async () => {
917
+ const root = ReactDOMClient.createRoot(document);
918
+ root.render(
919
+ <html>
920
+ <head />
921
+ <body>foo</body>
922
+ </html>,
923
+ );
924
+ expect(Scheduler).toFlushWithoutYielding();
925
+ expect(getVisibleChildren(document)).toEqual(
926
+ <html>
927
+ <head />
928
+ <body>foo</body>
929
+ </html>,
930
+ );
931
+
932
+ root.render(
933
+ <html>
934
+ <head />
935
+ <body>
936
+ <div>foo</div>
937
+ </body>
938
+ </html>,
939
+ );
940
+ expect(Scheduler).toFlushWithoutYielding();
941
+ expect(getVisibleChildren(document)).toEqual(
942
+ <html>
943
+ <head />
944
+ <body>
945
+ <div>foo</div>
946
+ </body>
947
+ </html>,
948
+ );
949
+
950
+ root.render(
951
+ <html>
952
+ <head />
953
+ <body>foo</body>
954
+ </html>,
955
+ );
956
+ expect(Scheduler).toFlushWithoutYielding();
957
+ expect(getVisibleChildren(document)).toEqual(
958
+ <html>
959
+ <head />
960
+ <body>foo</body>
961
+ </html>,
962
+ );
963
+
964
+ root.render(
965
+ <html>
966
+ <head />
967
+ <body>
968
+ <div>foo</div>
969
+ </body>
970
+ </html>,
971
+ );
972
+ expect(Scheduler).toFlushWithoutYielding();
973
+ expect(getVisibleChildren(document)).toEqual(
974
+ <html>
975
+ <head />
976
+ <body>
977
+ <div>foo</div>
978
+ </body>
979
+ </html>,
980
+ );
981
+ });
982
+});
packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+36
-2
@@ -62,7 +62,8 @@ describe('rendering React components at document', () => {
62
expect(body === testDocument.body).toBe(true);
63
});
64
65
- it('should not be able to unmount component from document node', () => {
65
+ // @gate enableHostSingletons
66
+ it('should be able to unmount component from document node, but leaves singleton nodes intact', () => {
67
class Root extends React.Component {
68
render() {
69
return (
@@ -81,7 +82,40 @@ describe('rendering React components at document', () => {
82
ReactDOM.hydrate(<Root />, testDocument);
83
expect(testDocument.body.innerHTML).toBe('Hello world');
84
84
- // In Fiber this actually works. It might not be a good idea though.
85
+ const originalDocEl = testDocument.documentElement;
86
+ const originalHead = testDocument.head;
87
+ const originalBody = testDocument.body;
88
+
89
+ // When we unmount everything is removed except the singleton nodes of html, head, and body
90
+ ReactDOM.unmountComponentAtNode(testDocument);
91
+ expect(testDocument.firstChild).toBe(originalDocEl);
92
+ expect(testDocument.head).toBe(originalHead);
93
+ expect(testDocument.body).toBe(originalBody);
94
+ expect(originalBody.firstChild).toEqual(null);
95
+ expect(originalHead.firstChild).toEqual(null);
96
+ });
97
+
98
+ // @gate !enableHostSingletons
99
+ it('should be able to unmount component from document node', () => {
100
+ class Root extends React.Component {
101
+ render() {
102
+ return (
103
+ <html>
104
+ <head>
105
+ <title>Hello World</title>
106
+ </head>
107
+ <body>Hello world</body>
108
+ </html>
109
+ );
110
+ }
111
+ }
112
+
113
+ const markup = ReactDOMServer.renderToString(<Root />);
114
+ const testDocument = getTestDocument(markup);
115
+ ReactDOM.hydrate(<Root />, testDocument);
116
+ expect(testDocument.body.innerHTML).toBe('Hello world');
117
+
118
+ // When we unmount everything is removed except the persistent nodes of html, head, and body
119
ReactDOM.unmountComponentAtNode(testDocument);
120
expect(testDocument.firstChild).toBe(null);
121
});
packages/react-dom/src/__tests__/validateDOMNesting-test.js
+44
-17
@@ -102,22 +102,49 @@ describe('validateDOMNesting', () => {
102
' in html (at **)',
103
],
104
);
105
- expectWarnings(
106
- ['body', 'body'],
107
- [
108
- 'render(): Rendering components directly into document.body is discouraged',
109
- 'validateDOMNesting(...): <body> cannot appear as a child of <body>.\n' +
110
- ' in body (at **)',
111
- ],
112
- 1,
113
- );
114
- expectWarnings(
115
- ['svg', 'foreignObject', 'body', 'p'],
116
- [
117
- 'validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.\n' +
118
- ' in body (at **)\n' +
119
- ' in foreignObject (at **)',
120
- ],
121
- );
105
+ if (gate(flags => flags.enableHostSingletons)) {
106
+ expectWarnings(
107
+ ['body', 'body'],
108
+ [
109
+ 'render(): Rendering components directly into document.body is discouraged',
110
+ 'validateDOMNesting(...): <body> cannot appear as a child of <body>.\n' +
111
+ ' in body (at **)',
112
+ 'Warning: You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
113
+ ' in body (at **)',
114
+ ],
115
+ 1,
116
+ );
117
+ } else {
118
+ expectWarnings(
119
+ ['body', 'body'],
120
+ [
121
+ 'render(): Rendering components directly into document.body is discouraged',
122
+ 'validateDOMNesting(...): <body> cannot appear as a child of <body>.\n' +
123
+ ' in body (at **)',
124
+ ],
125
+ 1,
126
+ );
127
+ }
128
+ if (gate(flags => flags.enableHostSingletons)) {
129
+ expectWarnings(
130
+ ['svg', 'foreignObject', 'body', 'p'],
131
+ [
132
+ 'validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.\n' +
133
+ ' in body (at **)\n' +
134
+ ' in foreignObject (at **)',
135
+ 'Warning: You are mounting a new body component when a previous one has not first unmounted. It is an error to render more than one body component at a time and attributes and children of these components will likely fail in unpredictable ways. Please only render a single instance of <body> and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
136
+ ' in body (at **)',
137
+ ],
138
+ );
139
+ } else {
140
+ expectWarnings(
141
+ ['svg', 'foreignObject', 'body', 'p'],
142
+ [
143
+ 'validateDOMNesting(...): <body> cannot appear as a child of <foreignObject>.\n' +
144
+ ' in body (at **)\n' +
145
+ ' in foreignObject (at **)',
146
+ ],
147
+ );
148
+ }
149
});
150
});
packages/react-dom/src/client/ReactDOMRoot.js
+6
-2
@@ -18,7 +18,7 @@ const {Dispatcher} = ReactDOMSharedInternals;
18
import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMFloatClient';
19
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
20
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
21
-import {enableFloat} from 'shared/ReactFeatureFlags';
21
+import {enableFloat, enableHostSingletons} from 'shared/ReactFeatureFlags';
22
23
export type RootType = {
24
render(children: ReactNodeList): void,
@@ -123,7 +123,11 @@ ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = functio
123
124
const container = root.containerInfo;
125
126
- if (!enableFloat && container.nodeType !== COMMENT_NODE) {
126
+ if (
127
+ !enableFloat &&
128
+ !enableHostSingletons &&
129
+ container.nodeType !== COMMENT_NODE
130
+ ) {
131
const hostInstance = findHostInstanceWithNoPortals(root.current);
132
if (hostInstance) {
133
if (hostInstance.parentNode !== container) {
packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+10
-1
@@ -58,6 +58,16 @@ describe('DOMPluginEventSystem', () => {
58
'enableLegacyFBSupport ' +
59
(enableLegacyFBSupport ? 'enabled' : 'disabled'),
60
() => {
61
+ beforeAll(() => {
62
+ // These tests are run twice, once with legacyFBSupport enabled and once disabled.
63
+ // The document needs to be cleaned up a bit before the second pass otherwise it is
64
+ // operating in a non pristine environment
65
+ document.removeChild(document.documentElement);
66
+ document.appendChild(document.createElement('html'));
67
+ document.documentElement.appendChild(document.createElement('head'));
68
+ document.documentElement.appendChild(document.createElement('body'));
69
+ });
70
+
71
beforeEach(() => {
72
jest.resetModules();
73
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -562,7 +572,6 @@ describe('DOMPluginEventSystem', () => {
572
}
573
574
ReactDOM.render(<Parent />, container);
565
-
575
const second = document.body.lastChild;
576
expect(second.textContent).toEqual('second');
577
dispatchClickEvent(second);
packages/react-dom/src/test-utils/ReactTestUtils.js
+9
-2
@@ -14,6 +14,7 @@ import {
14
FunctionComponent,
15
HostComponent,
16
HostResource,
17
+ HostSingleton,
18
HostText,
19
} from 'react-reconciler/src/ReactWorkTags';
20
import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
@@ -25,6 +26,7 @@ import {
26
import {enableFloat} from 'shared/ReactFeatureFlags';
27
import assign from 'shared/assign';
28
import isArray from 'shared/isArray';
29
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
30
31
// Keep in sync with ReactDOM.js:
32
const SecretInternals =
@@ -62,7 +64,8 @@ function findAllInRenderedFiberTreeInternal(fiber, test) {
64
node.tag === HostText ||
65
node.tag === ClassComponent ||
66
node.tag === FunctionComponent ||
65
- (enableFloat ? node.tag === HostResource : false)
67
+ (enableFloat ? node.tag === HostResource : false) ||
68
+ (enableHostSingletons ? node.tag === HostSingleton : false)
69
) {
70
const publicInst = node.stateNode;
71
if (test(publicInst)) {
@@ -415,7 +418,11 @@ function getParent(inst) {
418
// events to their parent. We could also go through parentNode on the
419
// host node but that wouldn't work for React Native and doesn't let us
420
// do the portal feature.
418
- } while (inst && inst.tag !== HostComponent);
421
+ } while (
422
+ inst &&
423
+ inst.tag !== HostComponent &&
424
+ (!enableHostSingletons ? true : inst.tag !== HostSingleton)
425
+ );
426
if (inst) {
427
return inst;
428
}
packages/react-native-renderer/src/ReactFabricHostConfig.js
+1
@@ -326,6 +326,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
326
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
327
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
328
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
329
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
330
331
export function appendInitialChild(
332
parentInstance: Instance,
packages/react-native-renderer/src/ReactNativeHostConfig.js
+1
@@ -90,6 +90,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
90
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
91
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
92
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
93
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
94
95
export function appendInitialChild(
96
parentInstance: Instance,
packages/react-noop-renderer/src/createReactNoop.js
+2
@@ -272,6 +272,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
272
}
273
274
const sharedHostConfig = {
275
+ supportsSingletons: false,
276
+
277
getRootHostContext() {
278
return NO_CONTEXT;
279
},
packages/react-reconciler/src/ReactFiber.new.js
+22
-2
@@ -21,7 +21,12 @@ import type {
21
} from './ReactFiberOffscreenComponent';
22
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
23
24
-import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
24
+import {
25
+ supportsResources,
26
+ supportsSingletons,
27
+ isHostResourceType,
28
+ isHostSingletonType,
29
+} from './ReactFiberHostConfig';
30
import {
31
createRootStrictEffectsByDefault,
32
enableCache,
@@ -34,6 +39,7 @@ import {
39
enableTransitionTracing,
40
enableDebugTracing,
41
enableFloat,
42
+ enableHostSingletons,
43
} from 'shared/ReactFeatureFlags';
44
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
45
import {ConcurrentRoot} from './ReactRootTags';
@@ -45,6 +51,7 @@ import {
51
HostText,
52
HostPortal,
53
HostResource,
54
+ HostSingleton,
55
ForwardRef,
56
Fragment,
57
Mode,
@@ -497,10 +504,23 @@ export function createFiberFromTypeAndProps(
504
}
505
}
506
} else if (typeof type === 'string') {
500
- if (enableFloat && supportsResources) {
507
+ if (
508
+ enableFloat &&
509
+ supportsResources &&
510
+ enableHostSingletons &&
511
+ supportsSingletons
512
+ ) {
513
+ fiberTag = isHostResourceType(type, pendingProps)
514
+ ? HostResource
515
+ : isHostSingletonType(type)
516
+ ? HostSingleton
517
+ : HostComponent;
518
+ } else if (enableFloat && supportsResources) {
519
fiberTag = isHostResourceType(type, pendingProps)
520
? HostResource
521
: HostComponent;
522
+ } else if (enableHostSingletons && supportsSingletons) {
523
+ fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent;
524
} else {
525
fiberTag = HostComponent;
526
}
packages/react-reconciler/src/ReactFiber.old.js
+22
-2
@@ -21,7 +21,12 @@ import type {
21
} from './ReactFiberOffscreenComponent';
22
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
23
24
-import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
24
+import {
25
+ supportsResources,
26
+ supportsSingletons,
27
+ isHostResourceType,
28
+ isHostSingletonType,
29
+} from './ReactFiberHostConfig';
30
import {
31
createRootStrictEffectsByDefault,
32
enableCache,
@@ -34,6 +39,7 @@ import {
39
enableTransitionTracing,
40
enableDebugTracing,
41
enableFloat,
42
+ enableHostSingletons,
43
} from 'shared/ReactFeatureFlags';
44
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
45
import {ConcurrentRoot} from './ReactRootTags';
@@ -45,6 +51,7 @@ import {
51
HostText,
52
HostPortal,
53
HostResource,
54
+ HostSingleton,
55
ForwardRef,
56
Fragment,
57
Mode,
@@ -497,10 +504,23 @@ export function createFiberFromTypeAndProps(
504
}
505
}
506
} else if (typeof type === 'string') {
500
- if (enableFloat && supportsResources) {
507
+ if (
508
+ enableFloat &&
509
+ supportsResources &&
510
+ enableHostSingletons &&
511
+ supportsSingletons
512
+ ) {
513
+ fiberTag = isHostResourceType(type, pendingProps)
514
+ ? HostResource
515
+ : isHostSingletonType(type)
516
+ ? HostSingleton
517
+ : HostComponent;
518
+ } else if (enableFloat && supportsResources) {
519
fiberTag = isHostResourceType(type, pendingProps)
520
? HostResource
521
: HostComponent;
522
+ } else if (enableHostSingletons && supportsSingletons) {
523
+ fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent;
524
} else {
525
fiberTag = HostComponent;
526
}
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+43
-5
@@ -37,11 +37,6 @@ import type {
37
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
38
import type {RootState} from './ReactFiberRoot.new';
39
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
40
-import {
41
- enableCPUSuspense,
42
- enableUseMutableSource,
43
- enableFloat,
44
-} from 'shared/ReactFeatureFlags';
40
41
import checkPropTypes from 'shared/checkPropTypes';
42
import {
@@ -56,6 +51,7 @@ import {
51
HostRoot,
52
HostComponent,
53
HostResource,
54
+ HostSingleton,
55
HostText,
56
HostPortal,
57
ForwardRef,
@@ -107,6 +103,10 @@ import {
103
enableSchedulingProfiler,
104
enableTransitionTracing,
105
enableLegacyHidden,
106
+ enableCPUSuspense,
107
+ enableUseMutableSource,
108
+ enableFloat,
109
+ enableHostSingletons,
110
} from 'shared/ReactFeatureFlags';
111
import isArray from 'shared/isArray';
112
import shallowEqual from 'shared/shallowEqual';
@@ -164,6 +164,7 @@ import {
164
registerSuspenseInstanceRetry,
165
supportsHydration,
166
supportsResources,
167
+ supportsSingletons,
168
isPrimaryRenderer,
169
getResource,
170
} from './ReactFiberHostConfig';
@@ -218,6 +219,7 @@ import {
219
enterHydrationState,
220
reenterHydrationStateFromDehydratedSuspenseInstance,
221
resetHydrationState,
222
+ claimHydratableSingleton,
223
tryToClaimNextHydratableInstance,
224
warnIfHydrating,
225
queueHydrationError,
@@ -1603,6 +1605,36 @@ function updateHostResource(current, workInProgress, renderLanes) {
1605
return workInProgress.child;
1606
}
1607
1608
+function updateHostSingleton(
1609
+ current: Fiber | null,
1610
+ workInProgress: Fiber,
1611
+ renderLanes: Lanes,
1612
+) {
1613
+ pushHostContext(workInProgress);
1614
+
1615
+ if (current === null) {
1616
+ claimHydratableSingleton(workInProgress);
1617
+ }
1618
+
1619
+ const nextChildren = workInProgress.pendingProps.children;
1620
+
1621
+ if (current === null && !getIsHydrating()) {
1622
+ // Similar to Portals we append Singleton children in the commit phase. So we
1623
+ // Track insertions even on mount.
1624
+ // TODO: Consider unifying this with how the root works.
1625
+ workInProgress.child = reconcileChildFibers(
1626
+ workInProgress,
1627
+ null,
1628
+ nextChildren,
1629
+ renderLanes,
1630
+ );
1631
+ } else {
1632
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1633
+ }
1634
+ markRef(current, workInProgress);
1635
+ return workInProgress.child;
1636
+}
1637
+
1638
function updateHostText(current, workInProgress) {
1639
if (current === null) {
1640
tryToClaimNextHydratableInstance(workInProgress);
@@ -3681,6 +3713,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3713
resetHydrationState();
3714
break;
3715
case HostResource:
3716
+ case HostSingleton:
3717
case HostComponent:
3718
pushHostContext(workInProgress);
3719
break;
@@ -4020,6 +4053,11 @@ function beginWork(
4053
return updateHostResource(current, workInProgress, renderLanes);
4054
}
4055
// eslint-disable-next-line no-fallthrough
4056
+ case HostSingleton:
4057
+ if (enableHostSingletons && supportsSingletons) {
4058
+ return updateHostSingleton(current, workInProgress, renderLanes);
4059
+ }
4060
+ // eslint-disable-next-line no-fallthrough
4061
case HostComponent:
4062
return updateHostComponent(current, workInProgress, renderLanes);
4063
case HostText:
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+43
-5
@@ -37,11 +37,6 @@ import type {
37
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
38
import type {RootState} from './ReactFiberRoot.old';
39
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
40
-import {
41
- enableCPUSuspense,
42
- enableUseMutableSource,
43
- enableFloat,
44
-} from 'shared/ReactFeatureFlags';
40
41
import checkPropTypes from 'shared/checkPropTypes';
42
import {
@@ -56,6 +51,7 @@ import {
51
HostRoot,
52
HostComponent,
53
HostResource,
54
+ HostSingleton,
55
HostText,
56
HostPortal,
57
ForwardRef,
@@ -107,6 +103,10 @@ import {
103
enableSchedulingProfiler,
104
enableTransitionTracing,
105
enableLegacyHidden,
106
+ enableCPUSuspense,
107
+ enableUseMutableSource,
108
+ enableFloat,
109
+ enableHostSingletons,
110
} from 'shared/ReactFeatureFlags';
111
import isArray from 'shared/isArray';
112
import shallowEqual from 'shared/shallowEqual';
@@ -164,6 +164,7 @@ import {
164
registerSuspenseInstanceRetry,
165
supportsHydration,
166
supportsResources,
167
+ supportsSingletons,
168
isPrimaryRenderer,
169
getResource,
170
} from './ReactFiberHostConfig';
@@ -218,6 +219,7 @@ import {
219
enterHydrationState,
220
reenterHydrationStateFromDehydratedSuspenseInstance,
221
resetHydrationState,
222
+ claimHydratableSingleton,
223
tryToClaimNextHydratableInstance,
224
warnIfHydrating,
225
queueHydrationError,
@@ -1603,6 +1605,36 @@ function updateHostResource(current, workInProgress, renderLanes) {
1605
return workInProgress.child;
1606
}
1607
1608
+function updateHostSingleton(
1609
+ current: Fiber | null,
1610
+ workInProgress: Fiber,
1611
+ renderLanes: Lanes,
1612
+) {
1613
+ pushHostContext(workInProgress);
1614
+
1615
+ if (current === null) {
1616
+ claimHydratableSingleton(workInProgress);
1617
+ }
1618
+
1619
+ const nextChildren = workInProgress.pendingProps.children;
1620
+
1621
+ if (current === null && !getIsHydrating()) {
1622
+ // Similar to Portals we append Singleton children in the commit phase. So we
1623
+ // Track insertions even on mount.
1624
+ // TODO: Consider unifying this with how the root works.
1625
+ workInProgress.child = reconcileChildFibers(
1626
+ workInProgress,
1627
+ null,
1628
+ nextChildren,
1629
+ renderLanes,
1630
+ );
1631
+ } else {
1632
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
1633
+ }
1634
+ markRef(current, workInProgress);
1635
+ return workInProgress.child;
1636
+}
1637
+
1638
function updateHostText(current, workInProgress) {
1639
if (current === null) {
1640
tryToClaimNextHydratableInstance(workInProgress);
@@ -3681,6 +3713,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
3713
resetHydrationState();
3714
break;
3715
case HostResource:
3716
+ case HostSingleton:
3717
case HostComponent:
3718
pushHostContext(workInProgress);
3719
break;
@@ -4020,6 +4053,11 @@ function beginWork(
4053
return updateHostResource(current, workInProgress, renderLanes);
4054
}
4055
// eslint-disable-next-line no-fallthrough
4056
+ case HostSingleton:
4057
+ if (enableHostSingletons && supportsSingletons) {
4058
+ return updateHostSingleton(current, workInProgress, renderLanes);
4059
+ }
4060
+ // eslint-disable-next-line no-fallthrough
4061
case HostComponent:
4062
return updateHostComponent(current, workInProgress, renderLanes);
4063
case HostText:
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+104
-7
@@ -53,6 +53,7 @@ import {
53
enableStrictEffects,
54
enableFloat,
55
enableLegacyHidden,
56
+ enableHostSingletons,
57
} from 'shared/ReactFeatureFlags';
58
import {
59
FunctionComponent,
@@ -61,6 +62,7 @@ import {
62
HostRoot,
63
HostComponent,
64
HostResource,
65
+ HostSingleton,
66
HostText,
67
HostPortal,
68
Profiler,
@@ -120,6 +122,7 @@ import {
122
supportsPersistence,
123
supportsHydration,
124
supportsResources,
125
+ supportsSingletons,
126
commitMount,
127
commitUpdate,
128
resetTextContent,
@@ -147,6 +150,9 @@ import {
150
detachDeletedInstance,
151
acquireResource,
152
releaseResource,
153
+ clearSingleton,
154
+ acquireSingletonInstance,
155
+ releaseSingletonInstance,
156
} from './ReactFiberHostConfig';
157
import {
158
captureCommitPhaseError,
@@ -501,6 +507,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
507
}
508
case HostComponent:
509
case HostResource:
510
+ case HostSingleton:
511
case HostText:
512
case HostPortal:
513
case IncompleteClassComponent:
@@ -1053,6 +1060,7 @@ function commitLayoutEffectOnFiber(
1060
let instance = null;
1061
if (finishedWork.child !== null) {
1062
switch (finishedWork.child.tag) {
1063
+ case HostSingleton:
1064
case HostComponent:
1065
instance = getPublicInstance(finishedWork.child.stateNode);
1066
break;
@@ -1098,6 +1106,7 @@ function commitLayoutEffectOnFiber(
1106
}
1107
}
1108
// eslint-disable-next-line-no-fallthrough
1109
+ case HostSingleton:
1110
case HostComponent: {
1111
recursivelyTraverseLayoutEffects(
1112
finishedRoot,
@@ -1487,7 +1496,12 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1496
while (true) {
1497
if (
1498
node.tag === HostComponent ||
1490
- (enableFloat && supportsResources ? node.tag === HostResource : false)
1499
+ (enableFloat && supportsResources
1500
+ ? node.tag === HostResource
1501
+ : false) ||
1502
+ (enableHostSingletons && supportsSingletons
1503
+ ? node.tag === HostSingleton
1504
+ : false)
1505
) {
1506
if (hostSubtreeRoot === null) {
1507
hostSubtreeRoot = node;
@@ -1561,6 +1575,7 @@ function commitAttachRef(finishedWork: Fiber) {
1575
let instanceToUse;
1576
switch (finishedWork.tag) {
1577
case HostResource:
1578
+ case HostSingleton:
1579
case HostComponent:
1580
instanceToUse = getPublicInstance(instance);
1581
break;
@@ -1765,8 +1780,11 @@ function isHostParent(fiber: Fiber): boolean {
1780
return (
1781
fiber.tag === HostComponent ||
1782
fiber.tag === HostRoot ||
1768
- fiber.tag === HostPortal ||
1769
- (enableFloat && supportsResources ? fiber.tag === HostResource : false)
1783
+ (enableFloat && supportsResources ? fiber.tag === HostResource : false) ||
1784
+ (enableHostSingletons && supportsSingletons
1785
+ ? fiber.tag === HostSingleton
1786
+ : false) ||
1787
+ fiber.tag === HostPortal
1788
);
1789
}
1790
@@ -1792,6 +1810,9 @@ function getHostSibling(fiber: Fiber): ?Instance {
1810
while (
1811
node.tag !== HostComponent &&
1812
node.tag !== HostText &&
1813
+ (!(enableHostSingletons && supportsSingletons)
1814
+ ? true
1815
+ : node.tag !== HostSingleton) &&
1816
node.tag !== DehydratedFragment
1817
) {
1818
// If it is not host node and, we might have a host node inside it.
@@ -1822,11 +1843,29 @@ function commitPlacement(finishedWork: Fiber): void {
1843
return;
1844
}
1845
1846
+ if (enableHostSingletons && supportsSingletons) {
1847
+ if (finishedWork.tag === HostSingleton) {
1848
+ // Singletons are already in the Host and don't need to be placed
1849
+ // Since they operate somewhat like Portals though their children will
1850
+ // have Placement and will get placed inside them
1851
+ return;
1852
+ }
1853
+ }
1854
// Recursively insert all host nodes into the parent.
1855
const parentFiber = getHostParentFiber(finishedWork);
1856
1828
- // Note: these two variables *must* always be updated together.
1857
switch (parentFiber.tag) {
1858
+ case HostSingleton: {
1859
+ if (enableHostSingletons && supportsSingletons) {
1860
+ const parent: Instance = parentFiber.stateNode;
1861
+ const before = getHostSibling(finishedWork);
1862
+ // We only have the top Fiber that was inserted but we need to recurse down its
1863
+ // children to find all the terminal nodes.
1864
+ insertOrAppendPlacementNode(finishedWork, before, parent);
1865
+ break;
1866
+ }
1867
+ }
1868
+ // eslint-disable-next-line no-fallthrough
1869
case HostComponent: {
1870
const parent: Instance = parentFiber.stateNode;
1871
if (parentFiber.flags & ContentReset) {
@@ -1872,10 +1911,14 @@ function insertOrAppendPlacementNodeIntoContainer(
1911
} else {
1912
appendChildToContainer(parent, stateNode);
1913
}
1875
- } else if (tag === HostPortal) {
1914
+ } else if (
1915
+ tag === HostPortal ||
1916
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
1917
+ ) {
1918
// If the insertion itself is a portal, then we don't want to traverse
1919
// down its children. Instead, we'll get insertions from each child in
1920
// the portal directly.
1921
+ // If the insertion is a HostSingleton then it will be placed independently
1922
} else {
1923
const child = node.child;
1924
if (child !== null) {
@@ -1903,10 +1946,14 @@ function insertOrAppendPlacementNode(
1946
} else {
1947
appendChild(parent, stateNode);
1948
}
1906
- } else if (tag === HostPortal) {
1949
+ } else if (
1950
+ tag === HostPortal ||
1951
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
1952
+ ) {
1953
// If the insertion itself is a portal, then we don't want to traverse
1954
// down its children. Instead, we'll get insertions from each child in
1955
// the portal directly.
1956
+ // If the insertion is a HostSingleton then it will be placed independently
1957
} else {
1958
const child = node.child;
1959
if (child !== null) {
@@ -1954,6 +2001,7 @@ function commitDeletionEffects(
2001
let parent: null | Fiber = returnFiber;
2002
findParent: while (parent !== null) {
2003
switch (parent.tag) {
2004
+ case HostSingleton:
2005
case HostComponent: {
2006
hostParent = parent.stateNode;
2007
hostParentIsContainer = false;
@@ -2019,14 +2067,41 @@ function commitDeletionEffectsOnFiber(
2067
if (!offscreenSubtreeWasHidden) {
2068
safelyDetachRef(deletedFiber, nearestMountedAncestor);
2069
}
2070
+ recursivelyTraverseDeletionEffects(
2071
+ finishedRoot,
2072
+ nearestMountedAncestor,
2073
+ deletedFiber,
2074
+ );
2075
+ releaseResource(deletedFiber.memoizedState);
2076
+ return;
2077
+ }
2078
+ }
2079
+ // eslint-disable-next-line no-fallthrough
2080
+ case HostSingleton: {
2081
+ if (enableHostSingletons && supportsSingletons) {
2082
+ if (!offscreenSubtreeWasHidden) {
2083
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
2084
+ }
2085
2086
+ const prevHostParent = hostParent;
2087
+ const prevHostParentIsContainer = hostParentIsContainer;
2088
+ hostParent = deletedFiber.stateNode;
2089
recursivelyTraverseDeletionEffects(
2090
finishedRoot,
2091
nearestMountedAncestor,
2092
deletedFiber,
2093
);
2094
2029
- releaseResource(deletedFiber.memoizedState);
2095
+ // Normally this is called in passive unmount effect phase however with
2096
+ // HostSingleton we warn if you acquire one that is already associated to
2097
+ // a different fiber. To increase our chances of avoiding this, specifically
2098
+ // if you keyed a HostSingleton so there will be a delete followed by a Placement
2099
+ // we treat detach eagerly here
2100
+ releaseSingletonInstance(deletedFiber.stateNode);
2101
+
2102
+ hostParent = prevHostParent;
2103
+ hostParentIsContainer = prevHostParentIsContainer;
2104
+
2105
return;
2106
}
2107
}
@@ -2543,6 +2618,26 @@ function commitMutationEffectsOnFiber(
2618
}
2619
}
2620
// eslint-disable-next-line-no-fallthrough
2621
+ case HostSingleton: {
2622
+ if (enableHostSingletons && supportsSingletons) {
2623
+ if (flags & Update) {
2624
+ const previousWork = finishedWork.alternate;
2625
+ if (previousWork === null) {
2626
+ const singleton = finishedWork.stateNode;
2627
+ const props = finishedWork.memoizedProps;
2628
+ // This was a new mount, we need to clear and set initial properties
2629
+ clearSingleton(singleton);
2630
+ acquireSingletonInstance(
2631
+ finishedWork.type,
2632
+ props,
2633
+ singleton,
2634
+ finishedWork,
2635
+ );
2636
+ }
2637
+ }
2638
+ }
2639
+ }
2640
+ // eslint-disable-next-line-no-fallthrough
2641
case HostComponent: {
2642
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2643
commitReconciliationEffects(finishedWork);
@@ -2935,6 +3030,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3030
break;
3031
}
3032
case HostResource:
3033
+ case HostSingleton:
3034
case HostComponent: {
3035
// TODO (Offscreen) Check: flags & RefStatic
3036
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3035,6 +3131,7 @@ export function reappearLayoutEffects(
3131
// ...
3132
// }
3133
case HostResource:
3134
+ case HostSingleton:
3135
case HostComponent: {
3136
recursivelyTraverseReappearLayoutEffects(
3137
finishedRoot,
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+104
-7
@@ -53,6 +53,7 @@ import {
53
enableStrictEffects,
54
enableFloat,
55
enableLegacyHidden,
56
+ enableHostSingletons,
57
} from 'shared/ReactFeatureFlags';
58
import {
59
FunctionComponent,
@@ -61,6 +62,7 @@ import {
62
HostRoot,
63
HostComponent,
64
HostResource,
65
+ HostSingleton,
66
HostText,
67
HostPortal,
68
Profiler,
@@ -120,6 +122,7 @@ import {
122
supportsPersistence,
123
supportsHydration,
124
supportsResources,
125
+ supportsSingletons,
126
commitMount,
127
commitUpdate,
128
resetTextContent,
@@ -147,6 +150,9 @@ import {
150
detachDeletedInstance,
151
acquireResource,
152
releaseResource,
153
+ clearSingleton,
154
+ acquireSingletonInstance,
155
+ releaseSingletonInstance,
156
} from './ReactFiberHostConfig';
157
import {
158
captureCommitPhaseError,
@@ -501,6 +507,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
507
}
508
case HostComponent:
509
case HostResource:
510
+ case HostSingleton:
511
case HostText:
512
case HostPortal:
513
case IncompleteClassComponent:
@@ -1053,6 +1060,7 @@ function commitLayoutEffectOnFiber(
1060
let instance = null;
1061
if (finishedWork.child !== null) {
1062
switch (finishedWork.child.tag) {
1063
+ case HostSingleton:
1064
case HostComponent:
1065
instance = getPublicInstance(finishedWork.child.stateNode);
1066
break;
@@ -1098,6 +1106,7 @@ function commitLayoutEffectOnFiber(
1106
}
1107
}
1108
// eslint-disable-next-line-no-fallthrough
1109
+ case HostSingleton:
1110
case HostComponent: {
1111
recursivelyTraverseLayoutEffects(
1112
finishedRoot,
@@ -1487,7 +1496,12 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1496
while (true) {
1497
if (
1498
node.tag === HostComponent ||
1490
- (enableFloat && supportsResources ? node.tag === HostResource : false)
1499
+ (enableFloat && supportsResources
1500
+ ? node.tag === HostResource
1501
+ : false) ||
1502
+ (enableHostSingletons && supportsSingletons
1503
+ ? node.tag === HostSingleton
1504
+ : false)
1505
) {
1506
if (hostSubtreeRoot === null) {
1507
hostSubtreeRoot = node;
@@ -1561,6 +1575,7 @@ function commitAttachRef(finishedWork: Fiber) {
1575
let instanceToUse;
1576
switch (finishedWork.tag) {
1577
case HostResource:
1578
+ case HostSingleton:
1579
case HostComponent:
1580
instanceToUse = getPublicInstance(instance);
1581
break;
@@ -1765,8 +1780,11 @@ function isHostParent(fiber: Fiber): boolean {
1780
return (
1781
fiber.tag === HostComponent ||
1782
fiber.tag === HostRoot ||
1768
- fiber.tag === HostPortal ||
1769
- (enableFloat && supportsResources ? fiber.tag === HostResource : false)
1783
+ (enableFloat && supportsResources ? fiber.tag === HostResource : false) ||
1784
+ (enableHostSingletons && supportsSingletons
1785
+ ? fiber.tag === HostSingleton
1786
+ : false) ||
1787
+ fiber.tag === HostPortal
1788
);
1789
}
1790
@@ -1792,6 +1810,9 @@ function getHostSibling(fiber: Fiber): ?Instance {
1810
while (
1811
node.tag !== HostComponent &&
1812
node.tag !== HostText &&
1813
+ (!(enableHostSingletons && supportsSingletons)
1814
+ ? true
1815
+ : node.tag !== HostSingleton) &&
1816
node.tag !== DehydratedFragment
1817
) {
1818
// If it is not host node and, we might have a host node inside it.
@@ -1822,11 +1843,29 @@ function commitPlacement(finishedWork: Fiber): void {
1843
return;
1844
}
1845
1846
+ if (enableHostSingletons && supportsSingletons) {
1847
+ if (finishedWork.tag === HostSingleton) {
1848
+ // Singletons are already in the Host and don't need to be placed
1849
+ // Since they operate somewhat like Portals though their children will
1850
+ // have Placement and will get placed inside them
1851
+ return;
1852
+ }
1853
+ }
1854
// Recursively insert all host nodes into the parent.
1855
const parentFiber = getHostParentFiber(finishedWork);
1856
1828
- // Note: these two variables *must* always be updated together.
1857
switch (parentFiber.tag) {
1858
+ case HostSingleton: {
1859
+ if (enableHostSingletons && supportsSingletons) {
1860
+ const parent: Instance = parentFiber.stateNode;
1861
+ const before = getHostSibling(finishedWork);
1862
+ // We only have the top Fiber that was inserted but we need to recurse down its
1863
+ // children to find all the terminal nodes.
1864
+ insertOrAppendPlacementNode(finishedWork, before, parent);
1865
+ break;
1866
+ }
1867
+ }
1868
+ // eslint-disable-next-line no-fallthrough
1869
case HostComponent: {
1870
const parent: Instance = parentFiber.stateNode;
1871
if (parentFiber.flags & ContentReset) {
@@ -1872,10 +1911,14 @@ function insertOrAppendPlacementNodeIntoContainer(
1911
} else {
1912
appendChildToContainer(parent, stateNode);
1913
}
1875
- } else if (tag === HostPortal) {
1914
+ } else if (
1915
+ tag === HostPortal ||
1916
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
1917
+ ) {
1918
// If the insertion itself is a portal, then we don't want to traverse
1919
// down its children. Instead, we'll get insertions from each child in
1920
// the portal directly.
1921
+ // If the insertion is a HostSingleton then it will be placed independently
1922
} else {
1923
const child = node.child;
1924
if (child !== null) {
@@ -1903,10 +1946,14 @@ function insertOrAppendPlacementNode(
1946
} else {
1947
appendChild(parent, stateNode);
1948
}
1906
- } else if (tag === HostPortal) {
1949
+ } else if (
1950
+ tag === HostPortal ||
1951
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
1952
+ ) {
1953
// If the insertion itself is a portal, then we don't want to traverse
1954
// down its children. Instead, we'll get insertions from each child in
1955
// the portal directly.
1956
+ // If the insertion is a HostSingleton then it will be placed independently
1957
} else {
1958
const child = node.child;
1959
if (child !== null) {
@@ -1954,6 +2001,7 @@ function commitDeletionEffects(
2001
let parent: null | Fiber = returnFiber;
2002
findParent: while (parent !== null) {
2003
switch (parent.tag) {
2004
+ case HostSingleton:
2005
case HostComponent: {
2006
hostParent = parent.stateNode;
2007
hostParentIsContainer = false;
@@ -2019,14 +2067,41 @@ function commitDeletionEffectsOnFiber(
2067
if (!offscreenSubtreeWasHidden) {
2068
safelyDetachRef(deletedFiber, nearestMountedAncestor);
2069
}
2070
+ recursivelyTraverseDeletionEffects(
2071
+ finishedRoot,
2072
+ nearestMountedAncestor,
2073
+ deletedFiber,
2074
+ );
2075
+ releaseResource(deletedFiber.memoizedState);
2076
+ return;
2077
+ }
2078
+ }
2079
+ // eslint-disable-next-line no-fallthrough
2080
+ case HostSingleton: {
2081
+ if (enableHostSingletons && supportsSingletons) {
2082
+ if (!offscreenSubtreeWasHidden) {
2083
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
2084
+ }
2085
2086
+ const prevHostParent = hostParent;
2087
+ const prevHostParentIsContainer = hostParentIsContainer;
2088
+ hostParent = deletedFiber.stateNode;
2089
recursivelyTraverseDeletionEffects(
2090
finishedRoot,
2091
nearestMountedAncestor,
2092
deletedFiber,
2093
);
2094
2029
- releaseResource(deletedFiber.memoizedState);
2095
+ // Normally this is called in passive unmount effect phase however with
2096
+ // HostSingleton we warn if you acquire one that is already associated to
2097
+ // a different fiber. To increase our chances of avoiding this, specifically
2098
+ // if you keyed a HostSingleton so there will be a delete followed by a Placement
2099
+ // we treat detach eagerly here
2100
+ releaseSingletonInstance(deletedFiber.stateNode);
2101
+
2102
+ hostParent = prevHostParent;
2103
+ hostParentIsContainer = prevHostParentIsContainer;
2104
+
2105
return;
2106
}
2107
}
@@ -2543,6 +2618,26 @@ function commitMutationEffectsOnFiber(
2618
}
2619
}
2620
// eslint-disable-next-line-no-fallthrough
2621
+ case HostSingleton: {
2622
+ if (enableHostSingletons && supportsSingletons) {
2623
+ if (flags & Update) {
2624
+ const previousWork = finishedWork.alternate;
2625
+ if (previousWork === null) {
2626
+ const singleton = finishedWork.stateNode;
2627
+ const props = finishedWork.memoizedProps;
2628
+ // This was a new mount, we need to clear and set initial properties
2629
+ clearSingleton(singleton);
2630
+ acquireSingletonInstance(
2631
+ finishedWork.type,
2632
+ props,
2633
+ singleton,
2634
+ finishedWork,
2635
+ );
2636
+ }
2637
+ }
2638
+ }
2639
+ }
2640
+ // eslint-disable-next-line-no-fallthrough
2641
case HostComponent: {
2642
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2643
commitReconciliationEffects(finishedWork);
@@ -2935,6 +3030,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
3030
break;
3031
}
3032
case HostResource:
3033
+ case HostSingleton:
3034
case HostComponent: {
3035
// TODO (Offscreen) Check: flags & RefStatic
3036
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3035,6 +3131,7 @@ export function reappearLayoutEffects(
3131
// ...
3132
// }
3133
case HostResource:
3134
+ case HostSingleton:
3135
case HostComponent: {
3136
recursivelyTraverseReappearLayoutEffects(
3137
finishedRoot,
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+64
-3
@@ -33,6 +33,7 @@ import type {Cache} from './ReactFiberCacheComponent.new';
33
import {
34
enableSuspenseAvoidThisFallback,
35
enableLegacyHidden,
36
+ enableHostSingletons,
37
} from 'shared/ReactFeatureFlags';
38
39
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -46,6 +47,7 @@ import {
47
HostRoot,
48
HostComponent,
49
HostResource,
50
+ HostSingleton,
51
HostText,
52
HostPortal,
53
ContextProvider,
@@ -88,12 +90,14 @@ import {
90
import {
91
createInstance,
92
createTextInstance,
93
+ resolveSingletonInstance,
94
appendInitialChild,
95
finalizeInitialChildren,
96
prepareUpdate,
97
supportsMutation,
98
supportsPersistence,
99
supportsResources,
100
+ supportsSingletons,
101
cloneInstance,
102
cloneHiddenInstance,
103
cloneHiddenTextInstance,
@@ -227,10 +231,16 @@ if (supportsMutation) {
231
while (node !== null) {
232
if (node.tag === HostComponent || node.tag === HostText) {
233
appendInitialChild(parent, node.stateNode);
230
- } else if (node.tag === HostPortal) {
234
+ } else if (
235
+ node.tag === HostPortal ||
236
+ (enableHostSingletons && supportsSingletons
237
+ ? node.tag === HostSingleton
238
+ : false)
239
+ ) {
240
// If we have a portal child, then we don't want to traverse
241
// down its children. Instead, we'll get insertions from each child in
242
// the portal directly.
243
+ // If we have a HostSingleton it will be placed independently
244
} else if (node.child !== null) {
245
node.child.return = node;
246
node = node.child;
@@ -984,6 +994,59 @@ function completeWork(
994
}
995
}
996
// eslint-disable-next-line-no-fallthrough
997
+ case HostSingleton: {
998
+ if (enableHostSingletons && supportsSingletons) {
999
+ popHostContext(workInProgress);
1000
+ const rootContainerInstance = getRootHostContainer();
1001
+ const type = workInProgress.type;
1002
+ if (current !== null && workInProgress.stateNode != null) {
1003
+ updateHostComponent(current, workInProgress, type, newProps);
1004
+
1005
+ if (current.ref !== workInProgress.ref) {
1006
+ markRef(workInProgress);
1007
+ }
1008
+ } else {
1009
+ if (!newProps) {
1010
+ if (workInProgress.stateNode === null) {
1011
+ throw new Error(
1012
+ 'We must have new props for new mounts. This error is likely ' +
1013
+ 'caused by a bug in React. Please file an issue.',
1014
+ );
1015
+ }
1016
+
1017
+ // This can happen when we abort work.
1018
+ bubbleProperties(workInProgress);
1019
+ return null;
1020
+ }
1021
+
1022
+ const currentHostContext = getHostContext();
1023
+ const wasHydrated = popHydrationState(workInProgress);
1024
+ if (wasHydrated) {
1025
+ // We ignore the boolean indicating there is an updateQueue because
1026
+ // it is used only to set text children and HostSingletons do not
1027
+ // use them.
1028
+ prepareToHydrateHostInstance(workInProgress, currentHostContext);
1029
+ } else {
1030
+ workInProgress.stateNode = resolveSingletonInstance(
1031
+ type,
1032
+ newProps,
1033
+ rootContainerInstance,
1034
+ currentHostContext,
1035
+ true,
1036
+ );
1037
+ markUpdate(workInProgress);
1038
+ }
1039
+
1040
+ if (workInProgress.ref !== null) {
1041
+ // If there is a ref on a host node we need to schedule a callback
1042
+ markRef(workInProgress);
1043
+ }
1044
+ }
1045
+ bubbleProperties(workInProgress);
1046
+ return null;
1047
+ }
1048
+ }
1049
+ // eslint-disable-next-line-no-fallthrough
1050
case HostComponent: {
1051
popHostContext(workInProgress);
1052
const type = workInProgress.type;
@@ -1032,9 +1095,7 @@ function completeWork(
1095
currentHostContext,
1096
workInProgress,
1097
);
1035
-
1098
appendAllChildren(instance, workInProgress, false, false);
1037
-
1099
workInProgress.stateNode = instance;
1100
1101
// Certain renderers require commit-time effects for initial mount.
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+64
-3
@@ -33,6 +33,7 @@ import type {Cache} from './ReactFiberCacheComponent.old';
33
import {
34
enableSuspenseAvoidThisFallback,
35
enableLegacyHidden,
36
+ enableHostSingletons,
37
} from 'shared/ReactFeatureFlags';
38
39
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
@@ -46,6 +47,7 @@ import {
47
HostRoot,
48
HostComponent,
49
HostResource,
50
+ HostSingleton,
51
HostText,
52
HostPortal,
53
ContextProvider,
@@ -88,12 +90,14 @@ import {
90
import {
91
createInstance,
92
createTextInstance,
93
+ resolveSingletonInstance,
94
appendInitialChild,
95
finalizeInitialChildren,
96
prepareUpdate,
97
supportsMutation,
98
supportsPersistence,
99
supportsResources,
100
+ supportsSingletons,
101
cloneInstance,
102
cloneHiddenInstance,
103
cloneHiddenTextInstance,
@@ -227,10 +231,16 @@ if (supportsMutation) {
231
while (node !== null) {
232
if (node.tag === HostComponent || node.tag === HostText) {
233
appendInitialChild(parent, node.stateNode);
230
- } else if (node.tag === HostPortal) {
234
+ } else if (
235
+ node.tag === HostPortal ||
236
+ (enableHostSingletons && supportsSingletons
237
+ ? node.tag === HostSingleton
238
+ : false)
239
+ ) {
240
// If we have a portal child, then we don't want to traverse
241
// down its children. Instead, we'll get insertions from each child in
242
// the portal directly.
243
+ // If we have a HostSingleton it will be placed independently
244
} else if (node.child !== null) {
245
node.child.return = node;
246
node = node.child;
@@ -984,6 +994,59 @@ function completeWork(
994
}
995
}
996
// eslint-disable-next-line-no-fallthrough
997
+ case HostSingleton: {
998
+ if (enableHostSingletons && supportsSingletons) {
999
+ popHostContext(workInProgress);
1000
+ const rootContainerInstance = getRootHostContainer();
1001
+ const type = workInProgress.type;
1002
+ if (current !== null && workInProgress.stateNode != null) {
1003
+ updateHostComponent(current, workInProgress, type, newProps);
1004
+
1005
+ if (current.ref !== workInProgress.ref) {
1006
+ markRef(workInProgress);
1007
+ }
1008
+ } else {
1009
+ if (!newProps) {
1010
+ if (workInProgress.stateNode === null) {
1011
+ throw new Error(
1012
+ 'We must have new props for new mounts. This error is likely ' +
1013
+ 'caused by a bug in React. Please file an issue.',
1014
+ );
1015
+ }
1016
+
1017
+ // This can happen when we abort work.
1018
+ bubbleProperties(workInProgress);
1019
+ return null;
1020
+ }
1021
+
1022
+ const currentHostContext = getHostContext();
1023
+ const wasHydrated = popHydrationState(workInProgress);
1024
+ if (wasHydrated) {
1025
+ // We ignore the boolean indicating there is an updateQueue because
1026
+ // it is used only to set text children and HostSingletons do not
1027
+ // use them.
1028
+ prepareToHydrateHostInstance(workInProgress, currentHostContext);
1029
+ } else {
1030
+ workInProgress.stateNode = resolveSingletonInstance(
1031
+ type,
1032
+ newProps,
1033
+ rootContainerInstance,
1034
+ currentHostContext,
1035
+ true,
1036
+ );
1037
+ markUpdate(workInProgress);
1038
+ }
1039
+
1040
+ if (workInProgress.ref !== null) {
1041
+ // If there is a ref on a host node we need to schedule a callback
1042
+ markRef(workInProgress);
1043
+ }
1044
+ }
1045
+ bubbleProperties(workInProgress);
1046
+ return null;
1047
+ }
1048
+ }
1049
+ // eslint-disable-next-line-no-fallthrough
1050
case HostComponent: {
1051
popHostContext(workInProgress);
1052
const type = workInProgress.type;
@@ -1032,9 +1095,7 @@ function completeWork(
1095
currentHostContext,
1096
workInProgress,
1097
);
1035
-
1098
appendAllChildren(instance, workInProgress, false, false);
1037
-
1099
workInProgress.stateNode = instance;
1100
1101
// Certain renderers require commit-time effects for initial mount.
packages/react-reconciler/src/ReactFiberComponentStack.js
+2
@@ -12,6 +12,7 @@ import type {Fiber} from './ReactInternalTypes';
12
import {
13
HostComponent,
14
HostResource,
15
+ HostSingleton,
16
LazyComponent,
17
SuspenseComponent,
18
SuspenseListComponent,
@@ -36,6 +37,7 @@ function describeFiber(fiber: Fiber): string {
37
const source = __DEV__ ? fiber._debugSource : null;
38
switch (fiber.tag) {
39
case HostResource:
40
+ case HostSingleton:
41
case HostComponent:
42
return describeBuiltInComponentFrame(fiber.type, source, owner);
43
case LazyComponent:
packages/react-reconciler/src/ReactFiberHostConfigWithNoSingletons.js
new
+27
@@ -0,0 +1,27 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+// Renderers that don't support mutation
11
+// can re-export everything from this module.
12
+
13
+function shim(...args: any) {
14
+ throw new Error(
15
+ 'The current renderer does not support Singletons. ' +
16
+ 'This error is likely caused by a bug in React. ' +
17
+ 'Please file an issue.',
18
+ );
19
+}
20
+
21
+// Resources (when unsupported)
22
+export const supportsSingletons = false;
23
+export const resolveSingletonInstance = shim;
24
+export const clearSingleton = shim;
25
+export const acquireSingletonInstance = shim;
26
+export const releaseSingletonInstance = shim;
27
+export const isHostSingletonType = shim;
packages/react-reconciler/src/ReactFiberHotReloading.new.js
+8
-2
@@ -23,6 +23,7 @@ import type {
23
ScheduleRoot,
24
} from './ReactFiberHotReloading';
25
26
+import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
27
import {
28
flushSync,
29
scheduleUpdateOnFiber,
@@ -38,6 +39,7 @@ import {
39
ForwardRef,
40
HostComponent,
41
HostResource,
42
+ HostSingleton,
43
HostPortal,
44
HostRoot,
45
MemoComponent,
@@ -48,7 +50,7 @@ import {
50
REACT_MEMO_TYPE,
51
REACT_LAZY_TYPE,
52
} from 'shared/ReactSymbols';
51
-import {enableFloat} from 'shared/ReactFeatureFlags';
53
+import {supportsSingletons} from './ReactFiberHostConfig';
54
55
let resolveFamily: RefreshHandler | null = null;
56
let failedBoundaries: WeakSet<Fiber> | null = null;
@@ -425,6 +427,7 @@ function findHostInstancesForFiberShallowly(
427
let node = fiber;
428
while (true) {
429
switch (node.tag) {
430
+ case HostSingleton:
431
case HostComponent:
432
hostInstances.add(node.stateNode);
433
return;
@@ -453,7 +456,10 @@ function findChildHostInstancesForFiberShallowly(
456
while (true) {
457
if (
458
node.tag === HostComponent ||
456
- (enableFloat ? node.tag === HostResource : false)
459
+ (enableFloat ? node.tag === HostResource : false) ||
460
+ (enableHostSingletons && supportsSingletons
461
+ ? node.tag === HostSingleton
462
+ : false)
463
) {
464
// We got a match.
465
foundHostInstances = true;
packages/react-reconciler/src/ReactFiberHotReloading.old.js
+8
-2
@@ -23,6 +23,7 @@ import type {
23
ScheduleRoot,
24
} from './ReactFiberHotReloading';
25
26
+import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
27
import {
28
flushSync,
29
scheduleUpdateOnFiber,
@@ -38,6 +39,7 @@ import {
39
ForwardRef,
40
HostComponent,
41
HostResource,
42
+ HostSingleton,
43
HostPortal,
44
HostRoot,
45
MemoComponent,
@@ -48,7 +50,7 @@ import {
50
REACT_MEMO_TYPE,
51
REACT_LAZY_TYPE,
52
} from 'shared/ReactSymbols';
51
-import {enableFloat} from 'shared/ReactFeatureFlags';
53
+import {supportsSingletons} from './ReactFiberHostConfig';
54
55
let resolveFamily: RefreshHandler | null = null;
56
let failedBoundaries: WeakSet<Fiber> | null = null;
@@ -425,6 +427,7 @@ function findHostInstancesForFiberShallowly(
427
let node = fiber;
428
while (true) {
429
switch (node.tag) {
430
+ case HostSingleton:
431
case HostComponent:
432
hostInstances.add(node.stateNode);
433
return;
@@ -453,7 +456,10 @@ function findChildHostInstancesForFiberShallowly(
456
while (true) {
457
if (
458
node.tag === HostComponent ||
456
- (enableFloat ? node.tag === HostResource : false)
459
+ (enableFloat ? node.tag === HostResource : false) ||
460
+ (enableHostSingletons && supportsSingletons
461
+ ? node.tag === HostSingleton
462
+ : false)
463
) {
464
// We got a match.
465
foundHostInstances = true;
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+69
-11
@@ -23,6 +23,7 @@ import type {CapturedValue} from './ReactCapturedValue';
23
24
import {
25
HostComponent,
26
+ HostSingleton,
27
HostText,
28
HostRoot,
29
SuspenseComponent,
@@ -34,6 +35,7 @@ import {
35
NoFlags,
36
DidCapture,
37
} from './ReactFiberFlags';
38
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
39
40
import {
41
createFiberFromHostInstanceForDeletion,
@@ -42,6 +44,7 @@ import {
44
import {
45
shouldSetTextContent,
46
supportsHydration,
47
+ supportsSingletons,
48
canHydrateInstance,
49
canHydrateTextInstance,
50
canHydrateSuspenseInstance,
@@ -68,6 +71,7 @@ import {
71
didNotFindHydratableInstance,
72
didNotFindHydratableTextInstance,
73
didNotFindHydratableSuspenseInstance,
74
+ resolveSingletonInstance,
75
} from './ReactFiberHostConfig';
76
import {OffscreenLane} from './ReactFiberLane.new';
77
import {
@@ -75,6 +79,10 @@ import {
79
restoreSuspendedTreeContext,
80
} from './ReactFiberTreeContext.new';
81
import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
82
+import {
83
+ getRootHostContainer,
84
+ getHostContext,
85
+} from './ReactFiberHostContext.new';
86
87
// The deepest Fiber on the stack involved in a hydration context.
88
// This may have been an insertion or a hydration.
@@ -162,6 +170,7 @@ function warnUnhydratedInstance(
170
);
171
break;
172
}
173
+ case HostSingleton:
174
case HostComponent: {
175
const isConcurrentMode = (returnFiber.mode & ConcurrentMode) !== NoMode;
176
didNotHydrateInstance(
@@ -218,6 +227,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
227
case HostRoot: {
228
const parentContainer = returnFiber.stateNode.containerInfo;
229
switch (fiber.tag) {
230
+ case HostSingleton:
231
case HostComponent:
232
const type = fiber.type;
233
const props = fiber.pendingProps;
@@ -242,11 +252,13 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
252
}
253
break;
254
}
255
+ case HostSingleton:
256
case HostComponent: {
257
const parentType = returnFiber.type;
258
const parentProps = returnFiber.memoizedProps;
259
const parentInstance = returnFiber.stateNode;
260
switch (fiber.tag) {
261
+ case HostSingleton:
262
case HostComponent: {
263
const type = fiber.type;
264
const props = fiber.pendingProps;
@@ -293,6 +305,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
305
const parentInstance = suspenseState.dehydrated;
306
if (parentInstance !== null)
307
switch (fiber.tag) {
308
+ case HostSingleton:
309
case HostComponent:
310
const type = fiber.type;
311
const props = fiber.pendingProps;
@@ -329,6 +342,8 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
342
343
function tryHydrate(fiber, nextInstance) {
344
switch (fiber.tag) {
345
+ // HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
346
+ // you can find it inlined in claimHydratableSingleton
347
case HostComponent: {
348
const type = fiber.type;
349
const props = fiber.pendingProps;
@@ -400,6 +415,25 @@ function throwOnHydrationMismatch(fiber: Fiber) {
415
);
416
}
417
418
+function claimHydratableSingleton(fiber: Fiber): void {
419
+ if (enableHostSingletons && supportsSingletons) {
420
+ if (!isHydrating) {
421
+ return;
422
+ }
423
+ const currentRootContainer = getRootHostContainer();
424
+ const currentHostContext = getHostContext();
425
+ const instance = (fiber.stateNode = resolveSingletonInstance(
426
+ fiber.type,
427
+ fiber.pendingProps,
428
+ currentRootContainer,
429
+ currentHostContext,
430
+ false,
431
+ ));
432
+ hydrationParentFiber = fiber;
433
+ nextHydratableInstance = getFirstHydratableChild(instance);
434
+ }
435
+}
436
+
437
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
438
if (!isHydrating) {
439
return;
@@ -510,6 +544,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
544
);
545
break;
546
}
547
+ case HostSingleton:
548
case HostComponent: {
549
const parentType = returnFiber.type;
550
const parentProps = returnFiber.memoizedProps;
@@ -585,7 +620,10 @@ function popToNextHostParent(fiber: Fiber): void {
620
parent !== null &&
621
parent.tag !== HostComponent &&
622
parent.tag !== HostRoot &&
588
- parent.tag !== SuspenseComponent
623
+ parent.tag !== SuspenseComponent &&
624
+ (!(enableHostSingletons && supportsSingletons)
625
+ ? true
626
+ : parent.tag !== HostSingleton)
627
) {
628
parent = parent.return;
629
}
@@ -610,16 +648,35 @@ function popHydrationState(fiber: Fiber): boolean {
648
return false;
649
}
650
613
- // If we have any remaining hydratable nodes, we need to delete them now.
614
- // We only do this deeper than head and body since they tend to have random
615
- // other nodes in them. We also ignore components with pure text content in
616
- // side of them. We also don't delete anything inside the root container.
617
- if (
618
- fiber.tag !== HostRoot &&
619
- (fiber.tag !== HostComponent ||
620
- (shouldDeleteUnhydratedTailInstances(fiber.type) &&
621
- !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
622
- ) {
651
+ let shouldClear = false;
652
+ if (enableHostSingletons && supportsSingletons) {
653
+ // With float we never clear the Root, or Singleton instances. We also do not clear Instances
654
+ // that have singleton text content
655
+ if (
656
+ fiber.tag !== HostRoot &&
657
+ fiber.tag !== HostSingleton &&
658
+ !(
659
+ fiber.tag === HostComponent &&
660
+ shouldSetTextContent(fiber.type, fiber.memoizedProps)
661
+ )
662
+ ) {
663
+ shouldClear = true;
664
+ }
665
+ } else {
666
+ // If we have any remaining hydratable nodes, we need to delete them now.
667
+ // We only do this deeper than head and body since they tend to have random
668
+ // other nodes in them. We also ignore components with pure text content in
669
+ // side of them. We also don't delete anything inside the root container.
670
+ if (
671
+ fiber.tag !== HostRoot &&
672
+ (fiber.tag !== HostComponent ||
673
+ (shouldDeleteUnhydratedTailInstances(fiber.type) &&
674
+ !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
675
+ ) {
676
+ shouldClear = true;
677
+ }
678
+ }
679
+ if (shouldClear) {
680
let nextInstance = nextHydratableInstance;
681
if (nextInstance) {
682
if (shouldClientRenderOnMismatch(fiber)) {
@@ -695,6 +752,7 @@ export {
752
getIsHydrating,
753
reenterHydrationStateFromDehydratedSuspenseInstance,
754
resetHydrationState,
755
+ claimHydratableSingleton,
756
tryToClaimNextHydratableInstance,
757
prepareToHydrateHostInstance,
758
prepareToHydrateHostTextInstance,
packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+69
-11
@@ -23,6 +23,7 @@ import type {CapturedValue} from './ReactCapturedValue';
23
24
import {
25
HostComponent,
26
+ HostSingleton,
27
HostText,
28
HostRoot,
29
SuspenseComponent,
@@ -34,6 +35,7 @@ import {
35
NoFlags,
36
DidCapture,
37
} from './ReactFiberFlags';
38
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
39
40
import {
41
createFiberFromHostInstanceForDeletion,
@@ -42,6 +44,7 @@ import {
44
import {
45
shouldSetTextContent,
46
supportsHydration,
47
+ supportsSingletons,
48
canHydrateInstance,
49
canHydrateTextInstance,
50
canHydrateSuspenseInstance,
@@ -68,6 +71,7 @@ import {
71
didNotFindHydratableInstance,
72
didNotFindHydratableTextInstance,
73
didNotFindHydratableSuspenseInstance,
74
+ resolveSingletonInstance,
75
} from './ReactFiberHostConfig';
76
import {OffscreenLane} from './ReactFiberLane.old';
77
import {
@@ -75,6 +79,10 @@ import {
79
restoreSuspendedTreeContext,
80
} from './ReactFiberTreeContext.old';
81
import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
82
+import {
83
+ getRootHostContainer,
84
+ getHostContext,
85
+} from './ReactFiberHostContext.old';
86
87
// The deepest Fiber on the stack involved in a hydration context.
88
// This may have been an insertion or a hydration.
@@ -162,6 +170,7 @@ function warnUnhydratedInstance(
170
);
171
break;
172
}
173
+ case HostSingleton:
174
case HostComponent: {
175
const isConcurrentMode = (returnFiber.mode & ConcurrentMode) !== NoMode;
176
didNotHydrateInstance(
@@ -218,6 +227,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
227
case HostRoot: {
228
const parentContainer = returnFiber.stateNode.containerInfo;
229
switch (fiber.tag) {
230
+ case HostSingleton:
231
case HostComponent:
232
const type = fiber.type;
233
const props = fiber.pendingProps;
@@ -242,11 +252,13 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
252
}
253
break;
254
}
255
+ case HostSingleton:
256
case HostComponent: {
257
const parentType = returnFiber.type;
258
const parentProps = returnFiber.memoizedProps;
259
const parentInstance = returnFiber.stateNode;
260
switch (fiber.tag) {
261
+ case HostSingleton:
262
case HostComponent: {
263
const type = fiber.type;
264
const props = fiber.pendingProps;
@@ -293,6 +305,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
305
const parentInstance = suspenseState.dehydrated;
306
if (parentInstance !== null)
307
switch (fiber.tag) {
308
+ case HostSingleton:
309
case HostComponent:
310
const type = fiber.type;
311
const props = fiber.pendingProps;
@@ -329,6 +342,8 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
342
343
function tryHydrate(fiber, nextInstance) {
344
switch (fiber.tag) {
345
+ // HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
346
+ // you can find it inlined in claimHydratableSingleton
347
case HostComponent: {
348
const type = fiber.type;
349
const props = fiber.pendingProps;
@@ -400,6 +415,25 @@ function throwOnHydrationMismatch(fiber: Fiber) {
415
);
416
}
417
418
+function claimHydratableSingleton(fiber: Fiber): void {
419
+ if (enableHostSingletons && supportsSingletons) {
420
+ if (!isHydrating) {
421
+ return;
422
+ }
423
+ const currentRootContainer = getRootHostContainer();
424
+ const currentHostContext = getHostContext();
425
+ const instance = (fiber.stateNode = resolveSingletonInstance(
426
+ fiber.type,
427
+ fiber.pendingProps,
428
+ currentRootContainer,
429
+ currentHostContext,
430
+ false,
431
+ ));
432
+ hydrationParentFiber = fiber;
433
+ nextHydratableInstance = getFirstHydratableChild(instance);
434
+ }
435
+}
436
+
437
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
438
if (!isHydrating) {
439
return;
@@ -510,6 +544,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
544
);
545
break;
546
}
547
+ case HostSingleton:
548
case HostComponent: {
549
const parentType = returnFiber.type;
550
const parentProps = returnFiber.memoizedProps;
@@ -585,7 +620,10 @@ function popToNextHostParent(fiber: Fiber): void {
620
parent !== null &&
621
parent.tag !== HostComponent &&
622
parent.tag !== HostRoot &&
588
- parent.tag !== SuspenseComponent
623
+ parent.tag !== SuspenseComponent &&
624
+ (!(enableHostSingletons && supportsSingletons)
625
+ ? true
626
+ : parent.tag !== HostSingleton)
627
) {
628
parent = parent.return;
629
}
@@ -610,16 +648,35 @@ function popHydrationState(fiber: Fiber): boolean {
648
return false;
649
}
650
613
- // If we have any remaining hydratable nodes, we need to delete them now.
614
- // We only do this deeper than head and body since they tend to have random
615
- // other nodes in them. We also ignore components with pure text content in
616
- // side of them. We also don't delete anything inside the root container.
617
- if (
618
- fiber.tag !== HostRoot &&
619
- (fiber.tag !== HostComponent ||
620
- (shouldDeleteUnhydratedTailInstances(fiber.type) &&
621
- !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
622
- ) {
651
+ let shouldClear = false;
652
+ if (enableHostSingletons && supportsSingletons) {
653
+ // With float we never clear the Root, or Singleton instances. We also do not clear Instances
654
+ // that have singleton text content
655
+ if (
656
+ fiber.tag !== HostRoot &&
657
+ fiber.tag !== HostSingleton &&
658
+ !(
659
+ fiber.tag === HostComponent &&
660
+ shouldSetTextContent(fiber.type, fiber.memoizedProps)
661
+ )
662
+ ) {
663
+ shouldClear = true;
664
+ }
665
+ } else {
666
+ // If we have any remaining hydratable nodes, we need to delete them now.
667
+ // We only do this deeper than head and body since they tend to have random
668
+ // other nodes in them. We also ignore components with pure text content in
669
+ // side of them. We also don't delete anything inside the root container.
670
+ if (
671
+ fiber.tag !== HostRoot &&
672
+ (fiber.tag !== HostComponent ||
673
+ (shouldDeleteUnhydratedTailInstances(fiber.type) &&
674
+ !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
675
+ ) {
676
+ shouldClear = true;
677
+ }
678
+ }
679
+ if (shouldClear) {
680
let nextInstance = nextHydratableInstance;
681
if (nextInstance) {
682
if (shouldClientRenderOnMismatch(fiber)) {
@@ -695,6 +752,7 @@ export {
752
getIsHydrating,
753
reenterHydrationStateFromDehydratedSuspenseInstance,
754
resetHydrationState,
755
+ claimHydratableSingleton,
756
tryToClaimNextHydratableInstance,
757
prepareToHydrateHostInstance,
758
prepareToHydrateHostTextInstance,
packages/react-reconciler/src/ReactFiberReconciler.new.js
+2
@@ -32,6 +32,7 @@ import {
32
import {get as getInstance} from 'shared/ReactInstanceMap';
33
import {
34
HostComponent,
35
+ HostSingleton,
36
ClassComponent,
37
HostRoot,
38
SuspenseComponent,
@@ -405,6 +406,7 @@ export function getPublicRootInstance(
406
return null;
407
}
408
switch (containerFiber.child.tag) {
409
+ case HostSingleton:
410
case HostComponent:
411
return getPublicInstance(containerFiber.child.stateNode);
412
default:
packages/react-reconciler/src/ReactFiberReconciler.old.js
+2
@@ -32,6 +32,7 @@ import {
32
import {get as getInstance} from 'shared/ReactInstanceMap';
33
import {
34
HostComponent,
35
+ HostSingleton,
36
ClassComponent,
37
HostRoot,
38
SuspenseComponent,
@@ -405,6 +406,7 @@ export function getPublicRootInstance(
406
return null;
407
}
408
switch (containerFiber.child.tag) {
409
+ case HostSingleton:
410
case HostComponent:
411
return getPublicInstance(containerFiber.child.stateNode);
412
default:
packages/react-reconciler/src/ReactFiberTreeReflection.js
+12
-7
@@ -18,13 +18,14 @@ import {
18
ClassComponent,
19
HostComponent,
20
HostResource,
21
+ HostSingleton,
22
HostRoot,
23
HostPortal,
24
HostText,
25
SuspenseComponent,
26
} from './ReactWorkTags';
27
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
27
-import {enableFloat} from 'shared/ReactFeatureFlags';
28
+import {enableFloat, enableHostSingletons} from 'shared/ReactFeatureFlags';
29
30
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
31
@@ -276,10 +277,12 @@ export function findCurrentHostFiber(parent: Fiber): Fiber | null {
277
278
function findCurrentHostFiberImpl(node: Fiber) {
279
// Next we'll drill down this component to find the first HostComponent/Text.
280
+ const tag = node.tag;
281
if (
280
- node.tag === HostComponent ||
281
- node.tag === HostText ||
282
- (enableFloat ? node.tag === HostResource : false)
282
+ tag === HostComponent ||
283
+ (enableFloat ? tag === HostResource : false) ||
284
+ (enableHostSingletons ? tag === HostSingleton : false) ||
285
+ tag === HostText
286
) {
287
return node;
288
}
@@ -305,10 +308,12 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
308
309
function findCurrentHostFiberWithNoPortalsImpl(node: Fiber) {
310
// Next we'll drill down this component to find the first HostComponent/Text.
311
+ const tag = node.tag;
312
if (
309
- node.tag === HostComponent ||
310
- node.tag === HostText ||
311
- (enableFloat ? node.tag === HostResource : false)
313
+ tag === HostComponent ||
314
+ (enableFloat ? tag === HostResource : false) ||
315
+ (enableHostSingletons ? tag === HostSingleton : false) ||
316
+ tag === HostText
317
) {
318
return node;
319
}
packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+3
@@ -20,6 +20,7 @@ import {
20
HostRoot,
21
HostComponent,
22
HostResource,
23
+ HostSingleton,
24
HostPortal,
25
ContextProvider,
26
SuspenseComponent,
@@ -117,6 +118,7 @@ function unwindWork(
118
return null;
119
}
120
case HostResource:
121
+ case HostSingleton:
122
case HostComponent: {
123
// TODO: popHydrationState
124
popHostContext(workInProgress);
@@ -236,6 +238,7 @@ function unwindInterruptedWork(
238
break;
239
}
240
case HostResource:
241
+ case HostSingleton:
242
case HostComponent: {
243
popHostContext(interruptedWork);
244
break;
packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+3
@@ -20,6 +20,7 @@ import {
20
HostRoot,
21
HostComponent,
22
HostResource,
23
+ HostSingleton,
24
HostPortal,
25
ContextProvider,
26
SuspenseComponent,
@@ -117,6 +118,7 @@ function unwindWork(
118
return null;
119
}
120
case HostResource:
121
+ case HostSingleton:
122
case HostComponent: {
123
// TODO: popHydrationState
124
popHostContext(workInProgress);
@@ -236,6 +238,7 @@ function unwindInterruptedWork(
238
break;
239
}
240
case HostResource:
241
+ case HostSingleton:
242
case HostComponent: {
243
popHostContext(interruptedWork);
244
break;
packages/react-reconciler/src/ReactTestSelectors.js
+40
-10
@@ -13,6 +13,7 @@ import type {Instance} from './ReactFiberHostConfig';
13
import {
14
HostComponent,
15
HostResource,
16
+ HostSingleton,
17
HostText,
18
} from 'react-reconciler/src/ReactWorkTags';
19
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -142,6 +143,7 @@ function findFiberRootForHostRoot(hostRoot: Instance): Fiber {
143
}
144
145
function matchSelector(fiber: Fiber, selector: Selector): boolean {
146
+ const tag = fiber.tag;
147
switch (selector.$$typeof) {
148
case COMPONENT_TYPE:
149
if (fiber.type === selector.value) {
@@ -154,7 +156,11 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
156
((selector: any): HasPseudoClassSelector).value,
157
);
158
case ROLE_TYPE:
157
- if (fiber.tag === HostComponent || fiber.tag === HostResource) {
159
+ if (
160
+ tag === HostComponent ||
161
+ tag === HostResource ||
162
+ tag === HostSingleton
163
+ ) {
164
const node = fiber.stateNode;
165
if (
166
matchAccessibilityRole(node, ((selector: any): RoleSelector).value)
@@ -165,9 +171,10 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
171
break;
172
case TEXT_TYPE:
173
if (
168
- fiber.tag === HostComponent ||
169
- fiber.tag === HostText ||
170
- fiber.tag === HostResource
174
+ tag === HostComponent ||
175
+ tag === HostText ||
176
+ tag === HostResource ||
177
+ tag === HostSingleton
178
) {
179
const textContent = getTextContent(fiber);
180
if (
@@ -179,7 +186,11 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
186
}
187
break;
188
case TEST_NAME_TYPE:
182
- if (fiber.tag === HostComponent || fiber.tag === HostResource) {
189
+ if (
190
+ tag === HostComponent ||
191
+ tag === HostResource ||
192
+ tag === HostSingleton
193
+ ) {
194
const dataTestID = fiber.memoizedProps['data-testname'];
195
if (
196
typeof dataTestID === 'string' &&
@@ -222,11 +233,14 @@ function findPaths(root: Fiber, selectors: Array<Selector>): Array<Fiber> {
233
let index = 0;
234
while (index < stack.length) {
235
const fiber = ((stack[index++]: any): Fiber);
236
+ const tag = fiber.tag;
237
let selectorIndex = ((stack[index++]: any): number);
238
let selector = selectors[selectorIndex];
239
240
if (
229
- (fiber.tag === HostComponent || fiber.tag === HostResource) &&
241
+ (tag === HostComponent ||
242
+ tag === HostResource ||
243
+ tag === HostSingleton) &&
244
isHiddenSubtree(fiber)
245
) {
246
continue;
@@ -257,11 +271,14 @@ function hasMatchingPaths(root: Fiber, selectors: Array<Selector>): boolean {
271
let index = 0;
272
while (index < stack.length) {
273
const fiber = ((stack[index++]: any): Fiber);
274
+ const tag = fiber.tag;
275
let selectorIndex = ((stack[index++]: any): number);
276
let selector = selectors[selectorIndex];
277
278
if (
264
- (fiber.tag === HostComponent || fiber.tag === HostResource) &&
279
+ (tag === HostComponent ||
280
+ tag === HostResource ||
281
+ tag === HostSingleton) &&
282
isHiddenSubtree(fiber)
283
) {
284
continue;
@@ -303,7 +320,12 @@ export function findAllNodes(
320
let index = 0;
321
while (index < stack.length) {
322
const node = ((stack[index++]: any): Fiber);
306
- if (node.tag === HostComponent || node.tag === HostResource) {
323
+ const tag = node.tag;
324
+ if (
325
+ tag === HostComponent ||
326
+ tag === HostResource ||
327
+ tag === HostSingleton
328
+ ) {
329
if (isHiddenSubtree(node)) {
330
continue;
331
}
@@ -338,11 +360,14 @@ export function getFindAllNodesFailureDescription(
360
let index = 0;
361
while (index < stack.length) {
362
const fiber = ((stack[index++]: any): Fiber);
363
+ const tag = fiber.tag;
364
let selectorIndex = ((stack[index++]: any): number);
365
const selector = selectors[selectorIndex];
366
367
if (
345
- (fiber.tag === HostComponent || fiber.tag === HostResource) &&
368
+ (tag === HostComponent ||
369
+ tag === HostResource ||
370
+ tag === HostSingleton) &&
371
isHiddenSubtree(fiber)
372
) {
373
continue;
@@ -493,10 +518,15 @@ export function focusWithin(
518
let index = 0;
519
while (index < stack.length) {
520
const fiber = ((stack[index++]: any): Fiber);
521
+ const tag = fiber.tag;
522
if (isHiddenSubtree(fiber)) {
523
continue;
524
}
499
- if (fiber.tag === HostComponent || fiber.tag === HostResource) {
525
+ if (
526
+ tag === HostComponent ||
527
+ tag === HostResource ||
528
+ tag === HostSingleton
529
+ ) {
530
const node = fiber.stateNode;
531
if (setFocusIfFocusable(node)) {
532
return true;
packages/react-reconciler/src/ReactWorkTags.js
+3
-1
@@ -34,7 +34,8 @@ export type WorkTag =
34
| 23
35
| 24
36
| 25
37
- | 26;
37
+ | 26
38
+ | 27;
39
40
export const FunctionComponent = 0;
41
export const ClassComponent = 1;
@@ -62,3 +63,4 @@ export const LegacyHiddenComponent = 23;
63
export const CacheComponent = 24;
64
export const TracingMarkerComponent = 25;
65
export const HostResource = 26;
66
+export const HostSingleton = 27;
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+11
@@ -199,3 +199,14 @@ export const isHostResourceType = $$$hostConfig.isHostResourceType;
199
export const getResource = $$$hostConfig.getResource;
200
export const acquireResource = $$$hostConfig.acquireResource;
201
export const releaseResource = $$$hostConfig.releaseResource;
202
+
203
+// -------------------
204
+// Singletons
205
+// (optional)
206
+// -------------------
207
+export const supportsSingletons = $$$hostConfig.supportsSingletons;
208
+export const resolveSingletonInstance = $$$hostConfig.resolveSingletonInstance;
209
+export const clearSingleton = $$$hostConfig.clearSingleton;
210
+export const acquireSingletonInstance = $$$hostConfig.acquireSingletonInstance;
211
+export const releaseSingletonInstance = $$$hostConfig.releaseSingletonInstance;
212
+export const isHostSingletonType = $$$hostConfig.isHostSingletonType;
packages/react-reconciler/src/getComponentNameFromFiber.js
+2
@@ -20,6 +20,7 @@ import {
20
HostPortal,
21
HostComponent,
22
HostResource,
23
+ HostSingleton,
24
HostText,
25
Fragment,
26
Mode,
@@ -79,6 +80,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
80
case Fragment:
81
return 'Fragment';
82
case HostResource:
83
+ case HostSingleton:
84
case HostComponent:
85
// Host component type is the display name (e.g. "div", "View")
86
return type;
packages/react-test-renderer/src/ReactTestHostConfig.js
+1
@@ -47,6 +47,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
47
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
48
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
49
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
50
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
51
52
const NO_CONTEXT = {};
53
const UPDATE_SIGNAL = {};
packages/react-test-renderer/src/ReactTestRenderer.js
+8
-1
@@ -32,6 +32,7 @@ import {
32
ClassComponent,
33
HostComponent,
34
HostResource,
35
+ HostSingleton,
36
HostPortal,
37
HostText,
38
HostRoot,
@@ -202,6 +203,7 @@ function toTree(node: ?Fiber) {
203
rendered: childrenToTree(node.child),
204
};
205
case HostResource:
206
+ case HostSingleton:
207
case HostComponent: {
208
return {
209
nodeType: 'host',
@@ -308,7 +310,12 @@ class ReactTestInstance {
310
}
311
312
get instance(): $FlowFixMe {
311
- if (this._fiber.tag === HostComponent || this._fiber.tag === HostResource) {
313
+ const tag = this._fiber.tag;
314
+ if (
315
+ tag === HostComponent ||
316
+ tag === HostResource ||
317
+ tag === HostSingleton
318
+ ) {
319
return getPublicInstance(this._fiber.stateNode);
320
} else {
321
return this._fiber.stateNode;
packages/shared/ReactFeatureFlags.js
+4
-1
@@ -100,6 +100,10 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
100
101
export const enableCPUSuspense = __EXPERIMENTAL__;
102
103
+export const enableHostSingletons = __EXPERIMENTAL__;
104
+
105
+export const enableFloat = __EXPERIMENTAL__;
106
+
107
// When a node is unmounted, recurse into the Fiber subtree and clean out
108
// references. Each level cleans up more fiber fields than the previous level.
109
// As far as we know, React itself doesn't leak, but because the Fiber contains
@@ -113,7 +117,6 @@ export const enableCPUSuspense = __EXPERIMENTAL__;
117
// aggressiveness.
118
export const deletedTreeCleanUpLevel = 3;
119
116
-export const enableFloat = __EXPERIMENTAL__;
120
export const enableUseHook = __EXPERIMENTAL__;
121
122
// Enables unstable_useMemoCache hook, intended as a compilation target for
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -84,6 +84,7 @@ export const enableUseMutableSource = true;
84
export const enableTransitionTracing = false;
85
86
export const enableFloat = false;
87
+export const enableHostSingletons = false;
88
89
export const useModernStrictMode = false;
90
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -73,6 +73,7 @@ export const enableUseMutableSource = false;
73
export const enableTransitionTracing = false;
74
75
export const enableFloat = false;
76
+export const enableHostSingletons = false;
77
78
export const useModernStrictMode = false;
79
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -73,6 +73,7 @@ export const enableUseMutableSource = false;
73
export const enableTransitionTracing = false;
74
75
export const enableFloat = false;
76
+export const enableHostSingletons = false;
77
78
export const useModernStrictMode = false;
79
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -71,6 +71,7 @@ export const enableUseMutableSource = false;
71
export const enableTransitionTracing = false;
72
73
export const enableFloat = false;
74
+export const enableHostSingletons = false;
75
76
export const useModernStrictMode = false;
77
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -75,6 +75,7 @@ export const enableUseMutableSource = true;
75
export const enableTransitionTracing = false;
76
77
export const enableFloat = false;
78
+export const enableHostSingletons = false;
79
80
export const useModernStrictMode = false;
81
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -73,6 +73,7 @@ export const enableUseMutableSource = false;
73
export const enableTransitionTracing = false;
74
75
export const enableFloat = false;
76
+export const enableHostSingletons = false;
77
78
export const useModernStrictMode = false;
79
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -74,6 +74,7 @@ export const enableUseMutableSource = true;
74
export const enableTransitionTracing = false;
75
76
export const enableFloat = false;
77
+export const enableHostSingletons = false;
78
79
export const useModernStrictMode = false;
80
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
-1
@@ -59,6 +59,5 @@ export const disableNativeComponentFrames = false;
59
export const createRootStrictEffectsByDefault = false;
60
export const enableStrictEffects = false;
61
export const allowConcurrentByDefault = true;
62
-export const enableFloat = false;
62
// You probably *don't* want to add more hardcoded ones.
63
// Instead, try to add them above with the __VARIANT__ value.
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -56,6 +56,7 @@ export const enableFloat = false;
56
export const enableUseHook = true;
57
export const enableUseMemoCacheHook = true;
58
export const enableUseEventHook = true;
59
+export const enableHostSingletons = false;
60
61
// Logs additional User Timing API marks for use with an experimental profiling tool.
62
export const enableSchedulingProfiler: boolean =
scripts/error-codes/codes.json
+6
-1
@@ -434,5 +434,10 @@
434
"446": "\"resourceRoot\" was expected to exist. This is a bug in React.",
435
"447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.",
436
"448": "createPortal was called on the server. Portals are not currently supported on the server. Update your program to conditionally call createPortal on the client only.",
437
- "449": "flushSync was called on the server. This is likely caused by a function being called during render or in module scope that was intended to be called from an effect or event handler. Update your to not call flushSync no the server."
437
+ "449": "flushSync was called on the server. This is likely caused by a function being called during render or in module scope that was intended to be called from an effect or event handler. Update your to not call flushSync no the server.",
438
+ "450": "The current renderer does not support Singletons. This error is likely caused by a bug in React. Please file an issue.",
439
+ "451": "resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.",
440
+ "452": "React expected an <html> element (document.documentElement) to exist in the Document but one was not found. React never removes the documentElement for any Document it renders into so the cause is likely in some other script running on this page.",
441
+ "453": "React expected a <head> element (document.head) to exist in the Document but one was not found. React never removes the head for any Document it renders into so the cause is likely in some other script running on this page.",
442
+ "454": "React expected a <body> element (document.body) to exist in the Document but one was not found. React never removes the body for any Document it renders into so the cause is likely in some other script running on this page."
443
}