@samitouri / QOS-React / commits / 2cf4352e1c

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 }