diff --git a/packages/react-art/src/ReactARTHostConfig.js b/packages/react-art/src/ReactARTHostConfig.js
index aa3925cdbe817..9746fe055c44d 100644
--- a/packages/react-art/src/ReactARTHostConfig.js
+++ b/packages/react-art/src/ReactARTHostConfig.js
@@ -244,6 +244,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
export function appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
index 25e37d01c2210..38e052b82e1e9 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
@@ -312,7 +312,8 @@ function setInitialDOMProperties(
// textContent on a
+ ,
+ );
+ pipe(writable);
+ });
+ container = document.head;
+
+ const root = ReactDOMClient.createRoot(container);
+ root.render(
something new );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ something new
+
+
+
+ this should be removed
+
+
+ ,
+ );
+ pipe(writable);
+ });
+ container = document.body;
+
+ let root;
+ // Given our new capabilities to render "safely" into the body we should consider removing this warning
+ expect(() => {
+ root = ReactDOMClient.createRoot(container);
+ }).toErrorDev(
+ '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.',
+ {withoutStack: true},
+ );
+ root.render(something new
);
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+ something new
+
+ ,
+ );
+ });
+
+ // @gate enableHostSingletons
+ it('renders single Text children into HostSingletons correctly', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+ foo
+ ,
+ );
+ pipe(writable);
+ });
+
+ let root = ReactDOMClient.hydrateRoot(
+ document,
+
+ foo
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+ foo
+ ,
+ );
+
+ root.render(
+
+ bar
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+ bar
+ ,
+ );
+
+ root.unmount();
+
+ root = ReactDOMClient.createRoot(document);
+ root.render(
+
+ baz
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+ baz
+ ,
+ );
+ });
+});
diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js
index 95b814b331dd9..c67f9564b60cc 100644
--- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js
+++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js
@@ -62,7 +62,8 @@ describe('rendering React components at document', () => {
expect(body === testDocument.body).toBe(true);
});
- it('should not be able to unmount component from document node', () => {
+ // @gate enableHostSingletons
+ it('should be able to unmount component from document node, but leaves singleton nodes intact', () => {
class Root extends React.Component {
render() {
return (
@@ -81,7 +82,40 @@ describe('rendering React components at document', () => {
ReactDOM.hydrate( , testDocument);
expect(testDocument.body.innerHTML).toBe('Hello world');
- // In Fiber this actually works. It might not be a good idea though.
+ const originalDocEl = testDocument.documentElement;
+ const originalHead = testDocument.head;
+ const originalBody = testDocument.body;
+
+ // When we unmount everything is removed except the singleton nodes of html, head, and body
+ ReactDOM.unmountComponentAtNode(testDocument);
+ expect(testDocument.firstChild).toBe(originalDocEl);
+ expect(testDocument.head).toBe(originalHead);
+ expect(testDocument.body).toBe(originalBody);
+ expect(originalBody.firstChild).toEqual(null);
+ expect(originalHead.firstChild).toEqual(null);
+ });
+
+ // @gate !enableHostSingletons
+ it('should be able to unmount component from document node', () => {
+ class Root extends React.Component {
+ render() {
+ return (
+
+
+ Hello World
+
+ Hello world
+
+ );
+ }
+ }
+
+ const markup = ReactDOMServer.renderToString( );
+ const testDocument = getTestDocument(markup);
+ ReactDOM.hydrate( , testDocument);
+ expect(testDocument.body.innerHTML).toBe('Hello world');
+
+ // When we unmount everything is removed except the persistent nodes of html, head, and body
ReactDOM.unmountComponentAtNode(testDocument);
expect(testDocument.firstChild).toBe(null);
});
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index ddd52b76865e9..1fc2e71eb860b 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -18,7 +18,7 @@ const {Dispatcher} = ReactDOMSharedInternals;
import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMFloatClient';
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
-import {enableFloat} from 'shared/ReactFeatureFlags';
+import {enableFloat, enableHostSingletons} from 'shared/ReactFeatureFlags';
export type RootType = {
render(children: ReactNodeList): void,
@@ -123,7 +123,11 @@ ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = functio
const container = root.containerInfo;
- if (!enableFloat && container.nodeType !== COMMENT_NODE) {
+ if (
+ !enableFloat &&
+ !enableHostSingletons &&
+ container.nodeType !== COMMENT_NODE
+ ) {
const hostInstance = findHostInstanceWithNoPortals(root.current);
if (hostInstance) {
if (hostInstance.parentNode !== container) {
diff --git a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
index bcc3f15c44e79..6fa2a18d44559 100644
--- a/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
+++ b/packages/react-dom/src/events/__tests__/DOMPluginEventSystem-test.internal.js
@@ -58,6 +58,16 @@ describe('DOMPluginEventSystem', () => {
'enableLegacyFBSupport ' +
(enableLegacyFBSupport ? 'enabled' : 'disabled'),
() => {
+ beforeAll(() => {
+ // These tests are run twice, once with legacyFBSupport enabled and once disabled.
+ // The document needs to be cleaned up a bit before the second pass otherwise it is
+ // operating in a non pristine environment
+ document.removeChild(document.documentElement);
+ document.appendChild(document.createElement('html'));
+ document.documentElement.appendChild(document.createElement('head'));
+ document.documentElement.appendChild(document.createElement('body'));
+ });
+
beforeEach(() => {
jest.resetModules();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
@@ -562,7 +572,6 @@ describe('DOMPluginEventSystem', () => {
}
ReactDOM.render( , container);
-
const second = document.body.lastChild;
expect(second.textContent).toEqual('second');
dispatchClickEvent(second);
diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js
index 22dbd6f4c0ed9..6d10bd7f4a6b8 100644
--- a/packages/react-dom/src/test-utils/ReactTestUtils.js
+++ b/packages/react-dom/src/test-utils/ReactTestUtils.js
@@ -14,6 +14,7 @@ import {
FunctionComponent,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
@@ -25,6 +26,7 @@ import {
import {enableFloat} from 'shared/ReactFeatureFlags';
import assign from 'shared/assign';
import isArray from 'shared/isArray';
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
// Keep in sync with ReactDOM.js:
const SecretInternals =
@@ -62,7 +64,8 @@ function findAllInRenderedFiberTreeInternal(fiber, test) {
node.tag === HostText ||
node.tag === ClassComponent ||
node.tag === FunctionComponent ||
- (enableFloat ? node.tag === HostResource : false)
+ (enableFloat ? node.tag === HostResource : false) ||
+ (enableHostSingletons ? node.tag === HostSingleton : false)
) {
const publicInst = node.stateNode;
if (test(publicInst)) {
@@ -415,7 +418,11 @@ function getParent(inst) {
// events to their parent. We could also go through parentNode on the
// host node but that wouldn't work for React Native and doesn't let us
// do the portal feature.
- } while (inst && inst.tag !== HostComponent);
+ } while (
+ inst &&
+ inst.tag !== HostComponent &&
+ (!enableHostSingletons ? true : inst.tag !== HostSingleton)
+ );
if (inst) {
return inst;
}
diff --git a/packages/react-native-renderer/src/ReactFabricHostConfig.js b/packages/react-native-renderer/src/ReactFabricHostConfig.js
index c0d17b3c24129..a9f84cc1dce1b 100644
--- a/packages/react-native-renderer/src/ReactFabricHostConfig.js
+++ b/packages/react-native-renderer/src/ReactFabricHostConfig.js
@@ -326,6 +326,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
export function appendInitialChild(
parentInstance: Instance,
diff --git a/packages/react-native-renderer/src/ReactNativeHostConfig.js b/packages/react-native-renderer/src/ReactNativeHostConfig.js
index 26855bd87e838..b54dd99d3b544 100644
--- a/packages/react-native-renderer/src/ReactNativeHostConfig.js
+++ b/packages/react-native-renderer/src/ReactNativeHostConfig.js
@@ -90,6 +90,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoScopes';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
export function appendInitialChild(
parentInstance: Instance,
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 46464a37f81c6..4b8fc08e26452 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -272,6 +272,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
}
const sharedHostConfig = {
+ supportsSingletons: false,
+
getRootHostContext() {
return NO_CONTEXT;
},
diff --git a/packages/react-reconciler/src/ReactFiber.new.js b/packages/react-reconciler/src/ReactFiber.new.js
index 18a3bf721b33c..57e10f9af2e31 100644
--- a/packages/react-reconciler/src/ReactFiber.new.js
+++ b/packages/react-reconciler/src/ReactFiber.new.js
@@ -21,7 +21,12 @@ import type {
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
-import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
+import {
+ supportsResources,
+ supportsSingletons,
+ isHostResourceType,
+ isHostSingletonType,
+} from './ReactFiberHostConfig';
import {
createRootStrictEffectsByDefault,
enableCache,
@@ -34,6 +39,7 @@ import {
enableTransitionTracing,
enableDebugTracing,
enableFloat,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@@ -45,6 +51,7 @@ import {
HostText,
HostPortal,
HostResource,
+ HostSingleton,
ForwardRef,
Fragment,
Mode,
@@ -497,10 +504,23 @@ export function createFiberFromTypeAndProps(
}
}
} else if (typeof type === 'string') {
- if (enableFloat && supportsResources) {
+ if (
+ enableFloat &&
+ supportsResources &&
+ enableHostSingletons &&
+ supportsSingletons
+ ) {
+ fiberTag = isHostResourceType(type, pendingProps)
+ ? HostResource
+ : isHostSingletonType(type)
+ ? HostSingleton
+ : HostComponent;
+ } else if (enableFloat && supportsResources) {
fiberTag = isHostResourceType(type, pendingProps)
? HostResource
: HostComponent;
+ } else if (enableHostSingletons && supportsSingletons) {
+ fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent;
} else {
fiberTag = HostComponent;
}
diff --git a/packages/react-reconciler/src/ReactFiber.old.js b/packages/react-reconciler/src/ReactFiber.old.js
index 94e0050e8b4d6..dac93beeec4c8 100644
--- a/packages/react-reconciler/src/ReactFiber.old.js
+++ b/packages/react-reconciler/src/ReactFiber.old.js
@@ -21,7 +21,12 @@ import type {
} from './ReactFiberOffscreenComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
-import {supportsResources, isHostResourceType} from './ReactFiberHostConfig';
+import {
+ supportsResources,
+ supportsSingletons,
+ isHostResourceType,
+ isHostSingletonType,
+} from './ReactFiberHostConfig';
import {
createRootStrictEffectsByDefault,
enableCache,
@@ -34,6 +39,7 @@ import {
enableTransitionTracing,
enableDebugTracing,
enableFloat,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {NoFlags, Placement, StaticMask} from './ReactFiberFlags';
import {ConcurrentRoot} from './ReactRootTags';
@@ -45,6 +51,7 @@ import {
HostText,
HostPortal,
HostResource,
+ HostSingleton,
ForwardRef,
Fragment,
Mode,
@@ -497,10 +504,23 @@ export function createFiberFromTypeAndProps(
}
}
} else if (typeof type === 'string') {
- if (enableFloat && supportsResources) {
+ if (
+ enableFloat &&
+ supportsResources &&
+ enableHostSingletons &&
+ supportsSingletons
+ ) {
+ fiberTag = isHostResourceType(type, pendingProps)
+ ? HostResource
+ : isHostSingletonType(type)
+ ? HostSingleton
+ : HostComponent;
+ } else if (enableFloat && supportsResources) {
fiberTag = isHostResourceType(type, pendingProps)
? HostResource
: HostComponent;
+ } else if (enableHostSingletons && supportsSingletons) {
+ fiberTag = isHostSingletonType(type) ? HostSingleton : HostComponent;
} else {
fiberTag = HostComponent;
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 9e9c4bd9700d0..34250b38ad4cc 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -37,11 +37,6 @@ import type {
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.new';
import type {RootState} from './ReactFiberRoot.new';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.new';
-import {
- enableCPUSuspense,
- enableUseMutableSource,
- enableFloat,
-} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
import {
@@ -56,6 +51,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
HostPortal,
ForwardRef,
@@ -107,6 +103,10 @@ import {
enableSchedulingProfiler,
enableTransitionTracing,
enableLegacyHidden,
+ enableCPUSuspense,
+ enableUseMutableSource,
+ enableFloat,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
@@ -164,6 +164,7 @@ import {
registerSuspenseInstanceRetry,
supportsHydration,
supportsResources,
+ supportsSingletons,
isPrimaryRenderer,
getResource,
} from './ReactFiberHostConfig';
@@ -218,6 +219,7 @@ import {
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
+ claimHydratableSingleton,
tryToClaimNextHydratableInstance,
warnIfHydrating,
queueHydrationError,
@@ -1603,6 +1605,36 @@ function updateHostResource(current, workInProgress, renderLanes) {
return workInProgress.child;
}
+function updateHostSingleton(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ renderLanes: Lanes,
+) {
+ pushHostContext(workInProgress);
+
+ if (current === null) {
+ claimHydratableSingleton(workInProgress);
+ }
+
+ const nextChildren = workInProgress.pendingProps.children;
+
+ if (current === null) {
+ // Similar to Portals we append Singleton children in the commit phase. So we
+ // Track insertions even on mount.
+ // TODO: Consider unifying this with how the root works.
+ workInProgress.child = reconcileChildFibers(
+ workInProgress,
+ null,
+ nextChildren,
+ renderLanes,
+ );
+ } else {
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ }
+ markRef(current, workInProgress);
+ return workInProgress.child;
+}
+
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
@@ -3681,6 +3713,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
resetHydrationState();
break;
case HostResource:
+ case HostSingleton:
case HostComponent:
pushHostContext(workInProgress);
break;
@@ -4020,6 +4053,11 @@ function beginWork(
return updateHostResource(current, workInProgress, renderLanes);
}
// eslint-disable-next-line no-fallthrough
+ case HostSingleton:
+ if (enableHostSingletons && supportsSingletons) {
+ return updateHostSingleton(current, workInProgress, renderLanes);
+ }
+ // eslint-disable-next-line no-fallthrough
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index bd12a989801f0..8a98a57437fd3 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -37,11 +37,6 @@ import type {
import type {UpdateQueue} from './ReactFiberClassUpdateQueue.old';
import type {RootState} from './ReactFiberRoot.old';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent.old';
-import {
- enableCPUSuspense,
- enableUseMutableSource,
- enableFloat,
-} from 'shared/ReactFeatureFlags';
import checkPropTypes from 'shared/checkPropTypes';
import {
@@ -56,6 +51,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
HostPortal,
ForwardRef,
@@ -107,6 +103,10 @@ import {
enableSchedulingProfiler,
enableTransitionTracing,
enableLegacyHidden,
+ enableCPUSuspense,
+ enableUseMutableSource,
+ enableFloat,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
@@ -164,6 +164,7 @@ import {
registerSuspenseInstanceRetry,
supportsHydration,
supportsResources,
+ supportsSingletons,
isPrimaryRenderer,
getResource,
} from './ReactFiberHostConfig';
@@ -218,6 +219,7 @@ import {
enterHydrationState,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
+ claimHydratableSingleton,
tryToClaimNextHydratableInstance,
warnIfHydrating,
queueHydrationError,
@@ -1603,6 +1605,36 @@ function updateHostResource(current, workInProgress, renderLanes) {
return workInProgress.child;
}
+function updateHostSingleton(
+ current: Fiber | null,
+ workInProgress: Fiber,
+ renderLanes: Lanes,
+) {
+ pushHostContext(workInProgress);
+
+ if (current === null) {
+ claimHydratableSingleton(workInProgress);
+ }
+
+ const nextChildren = workInProgress.pendingProps.children;
+
+ if (current === null) {
+ // Similar to Portals we append Singleton children in the commit phase. So we
+ // Track insertions even on mount.
+ // TODO: Consider unifying this with how the root works.
+ workInProgress.child = reconcileChildFibers(
+ workInProgress,
+ null,
+ nextChildren,
+ renderLanes,
+ );
+ } else {
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
+ }
+ markRef(current, workInProgress);
+ return workInProgress.child;
+}
+
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
@@ -3681,6 +3713,7 @@ function attemptEarlyBailoutIfNoScheduledUpdate(
resetHydrationState();
break;
case HostResource:
+ case HostSingleton:
case HostComponent:
pushHostContext(workInProgress);
break;
@@ -4020,6 +4053,11 @@ function beginWork(
return updateHostResource(current, workInProgress, renderLanes);
}
// eslint-disable-next-line no-fallthrough
+ case HostSingleton:
+ if (enableHostSingletons && supportsSingletons) {
+ return updateHostSingleton(current, workInProgress, renderLanes);
+ }
+ // eslint-disable-next-line no-fallthrough
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.new.js b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
index e0bd907706f61..2976b3675396a 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.new.js
@@ -53,6 +53,7 @@ import {
enableStrictEffects,
enableFloat,
enableLegacyHidden,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -61,6 +62,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
HostPortal,
Profiler,
@@ -120,6 +122,7 @@ import {
supportsPersistence,
supportsHydration,
supportsResources,
+ supportsSingletons,
commitMount,
commitUpdate,
resetTextContent,
@@ -147,6 +150,7 @@ import {
detachDeletedInstance,
acquireResource,
releaseResource,
+ acquireSingletonInstance,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -501,6 +505,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
}
case HostComponent:
case HostResource:
+ case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
@@ -1053,6 +1058,7 @@ function commitLayoutEffectOnFiber(
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
+ case HostSingleton:
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
@@ -1098,6 +1104,7 @@ function commitLayoutEffectOnFiber(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton:
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
@@ -1487,7 +1494,12 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
while (true) {
if (
node.tag === HostComponent ||
- (enableFloat && supportsResources ? node.tag === HostResource : false)
+ (enableFloat && supportsResources
+ ? node.tag === HostResource
+ : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? node.tag === HostSingleton
+ : false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
@@ -1561,6 +1573,7 @@ function commitAttachRef(finishedWork: Fiber) {
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
+ case HostSingleton:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
@@ -1693,6 +1706,10 @@ function detachFiberAfterEffects(fiber: Fiber) {
// tree, which has its own pointers to children, parents, and siblings.
// The other host nodes also point back to fibers, so we should detach that
// one, too.
+ // @TODO for HostSingletons we cannot safely detatch the statenode because
+ // it might actually be pointing to a different fiber. The right thing to do
+ // is probably detach if the stateNode is pointing to this fiber or its alternate
+ // but for now we will leave them dangling to consider the impact
if (fiber.tag === HostComponent) {
const hostInstance: Instance = fiber.stateNode;
if (hostInstance !== null) {
@@ -1766,7 +1783,10 @@ function isHostParent(fiber: Fiber): boolean {
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
fiber.tag === HostPortal ||
- (enableFloat && supportsResources ? fiber.tag === HostResource : false)
+ (enableFloat && supportsResources ? fiber.tag === HostResource : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? fiber.tag === HostSingleton
+ : false)
);
}
@@ -1792,7 +1812,10 @@ function getHostSibling(fiber: Fiber): ?Instance {
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
- node.tag !== DehydratedFragment
+ node.tag !== DehydratedFragment &&
+ (!(enableHostSingletons && supportsSingletons)
+ ? true
+ : node.tag !== HostSingleton)
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
@@ -1822,11 +1845,29 @@ function commitPlacement(finishedWork: Fiber): void {
return;
}
+ if (enableHostSingletons && supportsSingletons) {
+ if (finishedWork.tag === HostSingleton) {
+ // Singletons are already in the Host and don't need to be placed
+ // Since they operate somewhat like Portals though their children will
+ // have Placement and will get placed inside them
+ return;
+ }
+ }
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
- // Note: these two variables *must* always be updated together.
switch (parentFiber.tag) {
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ const parent: Instance = parentFiber.stateNode;
+ const before = getHostSibling(finishedWork);
+ // We only have the top Fiber that was inserted but we need to recurse down its
+ // children to find all the terminal nodes.
+ insertOrAppendPlacementNode(finishedWork, before, parent);
+ break;
+ }
+ }
+ // eslint-disable-next-line no-fallthrough
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
@@ -1872,10 +1913,14 @@ function insertOrAppendPlacementNodeIntoContainer(
} else {
appendChildToContainer(parent, stateNode);
}
- } else if (tag === HostPortal) {
+ } else if (
+ tag === HostPortal ||
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
+ ) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
+ // If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
@@ -1903,10 +1948,14 @@ function insertOrAppendPlacementNode(
} else {
appendChild(parent, stateNode);
}
- } else if (tag === HostPortal) {
+ } else if (
+ tag === HostPortal ||
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
+ ) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
+ // If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
@@ -1954,6 +2003,7 @@ function commitDeletionEffects(
let parent: null | Fiber = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
+ case HostSingleton:
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
@@ -2019,14 +2069,36 @@ function commitDeletionEffectsOnFiber(
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
+ recursivelyTraverseDeletionEffects(
+ finishedRoot,
+ nearestMountedAncestor,
+ deletedFiber,
+ );
+ releaseResource(deletedFiber.memoizedState);
+ return;
+ }
+ }
+ // eslint-disable-next-line no-fallthrough
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ if (!offscreenSubtreeWasHidden) {
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
+ }
+ const prevHostParent = hostParent;
+ const prevHostParentIsContainer = hostParentIsContainer;
+ hostParent = deletedFiber.stateNode;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
- releaseResource(deletedFiber.memoizedState);
+ hostParent = prevHostParent;
+ hostParentIsContainer = prevHostParentIsContainer;
+
+ clearContainer(deletedFiber.stateNode);
+
return;
}
}
@@ -2543,6 +2615,24 @@ function commitMutationEffectsOnFiber(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ if (flags & Update) {
+ const previousWork = finishedWork.alternate;
+ if (previousWork === null) {
+ const singleton = finishedWork.stateNode;
+ clearContainer(singleton);
+ acquireSingletonInstance(
+ finishedWork.type,
+ finishedWork.memoizedProps,
+ singleton,
+ finishedWork,
+ );
+ }
+ }
+ }
+ }
+ // eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
@@ -2935,6 +3025,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
break;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3035,6 +3126,7 @@ export function reappearLayoutEffects(
// ...
// }
case HostResource:
+ case HostSingleton:
case HostComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.old.js b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
index 0f449a81dc836..7bb9f232e7a15 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.old.js
@@ -53,6 +53,7 @@ import {
enableStrictEffects,
enableFloat,
enableLegacyHidden,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -61,6 +62,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
HostPortal,
Profiler,
@@ -120,6 +122,7 @@ import {
supportsPersistence,
supportsHydration,
supportsResources,
+ supportsSingletons,
commitMount,
commitUpdate,
resetTextContent,
@@ -147,6 +150,7 @@ import {
detachDeletedInstance,
acquireResource,
releaseResource,
+ acquireSingletonInstance,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -501,6 +505,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
}
case HostComponent:
case HostResource:
+ case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
@@ -1053,6 +1058,7 @@ function commitLayoutEffectOnFiber(
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
+ case HostSingleton:
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
@@ -1098,6 +1104,7 @@ function commitLayoutEffectOnFiber(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton:
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
@@ -1487,7 +1494,12 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
while (true) {
if (
node.tag === HostComponent ||
- (enableFloat && supportsResources ? node.tag === HostResource : false)
+ (enableFloat && supportsResources
+ ? node.tag === HostResource
+ : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? node.tag === HostSingleton
+ : false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
@@ -1561,6 +1573,7 @@ function commitAttachRef(finishedWork: Fiber) {
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
+ case HostSingleton:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
@@ -1693,6 +1706,10 @@ function detachFiberAfterEffects(fiber: Fiber) {
// tree, which has its own pointers to children, parents, and siblings.
// The other host nodes also point back to fibers, so we should detach that
// one, too.
+ // @TODO for HostSingletons we cannot safely detatch the statenode because
+ // it might actually be pointing to a different fiber. The right thing to do
+ // is probably detach if the stateNode is pointing to this fiber or its alternate
+ // but for now we will leave them dangling to consider the impact
if (fiber.tag === HostComponent) {
const hostInstance: Instance = fiber.stateNode;
if (hostInstance !== null) {
@@ -1766,7 +1783,10 @@ function isHostParent(fiber: Fiber): boolean {
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
fiber.tag === HostPortal ||
- (enableFloat && supportsResources ? fiber.tag === HostResource : false)
+ (enableFloat && supportsResources ? fiber.tag === HostResource : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? fiber.tag === HostSingleton
+ : false)
);
}
@@ -1792,7 +1812,10 @@ function getHostSibling(fiber: Fiber): ?Instance {
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
- node.tag !== DehydratedFragment
+ node.tag !== DehydratedFragment &&
+ (!(enableHostSingletons && supportsSingletons)
+ ? true
+ : node.tag !== HostSingleton)
) {
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
@@ -1822,11 +1845,29 @@ function commitPlacement(finishedWork: Fiber): void {
return;
}
+ if (enableHostSingletons && supportsSingletons) {
+ if (finishedWork.tag === HostSingleton) {
+ // Singletons are already in the Host and don't need to be placed
+ // Since they operate somewhat like Portals though their children will
+ // have Placement and will get placed inside them
+ return;
+ }
+ }
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
- // Note: these two variables *must* always be updated together.
switch (parentFiber.tag) {
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ const parent: Instance = parentFiber.stateNode;
+ const before = getHostSibling(finishedWork);
+ // We only have the top Fiber that was inserted but we need to recurse down its
+ // children to find all the terminal nodes.
+ insertOrAppendPlacementNode(finishedWork, before, parent);
+ break;
+ }
+ }
+ // eslint-disable-next-line no-fallthrough
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
@@ -1872,10 +1913,14 @@ function insertOrAppendPlacementNodeIntoContainer(
} else {
appendChildToContainer(parent, stateNode);
}
- } else if (tag === HostPortal) {
+ } else if (
+ tag === HostPortal ||
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
+ ) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
+ // If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
@@ -1903,10 +1948,14 @@ function insertOrAppendPlacementNode(
} else {
appendChild(parent, stateNode);
}
- } else if (tag === HostPortal) {
+ } else if (
+ tag === HostPortal ||
+ (enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
+ ) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
+ // If the insertion is a HostSingleton then it will be placed independently
} else {
const child = node.child;
if (child !== null) {
@@ -1954,6 +2003,7 @@ function commitDeletionEffects(
let parent: null | Fiber = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
+ case HostSingleton:
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
@@ -2019,14 +2069,36 @@ function commitDeletionEffectsOnFiber(
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
+ recursivelyTraverseDeletionEffects(
+ finishedRoot,
+ nearestMountedAncestor,
+ deletedFiber,
+ );
+ releaseResource(deletedFiber.memoizedState);
+ return;
+ }
+ }
+ // eslint-disable-next-line no-fallthrough
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ if (!offscreenSubtreeWasHidden) {
+ safelyDetachRef(deletedFiber, nearestMountedAncestor);
+ }
+ const prevHostParent = hostParent;
+ const prevHostParentIsContainer = hostParentIsContainer;
+ hostParent = deletedFiber.stateNode;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
- releaseResource(deletedFiber.memoizedState);
+ hostParent = prevHostParent;
+ hostParentIsContainer = prevHostParentIsContainer;
+
+ clearContainer(deletedFiber.stateNode);
+
return;
}
}
@@ -2543,6 +2615,24 @@ function commitMutationEffectsOnFiber(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ if (flags & Update) {
+ const previousWork = finishedWork.alternate;
+ if (previousWork === null) {
+ const singleton = finishedWork.stateNode;
+ clearContainer(singleton);
+ acquireSingletonInstance(
+ finishedWork.type,
+ finishedWork.memoizedProps,
+ singleton,
+ finishedWork,
+ );
+ }
+ }
+ }
+ }
+ // eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
@@ -2935,6 +3025,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
break;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3035,6 +3126,7 @@ export function reappearLayoutEffects(
// ...
// }
case HostResource:
+ case HostSingleton:
case HostComponent: {
recursivelyTraverseReappearLayoutEffects(
finishedRoot,
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index ef85cb32be9a2..0f6d05fcc500c 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -33,6 +33,7 @@ import type {Cache} from './ReactFiberCacheComponent.new';
import {
enableSuspenseAvoidThisFallback,
enableLegacyHidden,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
@@ -46,6 +47,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
HostPortal,
ContextProvider,
@@ -88,12 +90,14 @@ import {
import {
createInstance,
createTextInstance,
+ resolveSingletonInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
supportsMutation,
supportsPersistence,
supportsResources,
+ supportsSingletons,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
@@ -227,10 +231,16 @@ if (supportsMutation) {
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
- } else if (node.tag === HostPortal) {
+ } else if (
+ node.tag === HostPortal ||
+ (enableHostSingletons && supportsSingletons
+ ? node.tag === HostSingleton
+ : false)
+ ) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
+ // If we have a HostSingleton it will be placed independently
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
@@ -984,6 +994,59 @@ function completeWork(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ popHostContext(workInProgress);
+ const rootContainerInstance = getRootHostContainer();
+ const type = workInProgress.type;
+ if (current !== null && workInProgress.stateNode != null) {
+ updateHostComponent(current, workInProgress, type, newProps);
+
+ if (current.ref !== workInProgress.ref) {
+ markRef(workInProgress);
+ }
+ } else {
+ if (!newProps) {
+ if (workInProgress.stateNode === null) {
+ throw new Error(
+ 'We must have new props for new mounts. This error is likely ' +
+ 'caused by a bug in React. Please file an issue.',
+ );
+ }
+
+ // This can happen when we abort work.
+ bubbleProperties(workInProgress);
+ return null;
+ }
+
+ const currentHostContext = getHostContext();
+ const wasHydrated = popHydrationState(workInProgress);
+ if (wasHydrated) {
+ // This method returns an updateQueue. we intentionally ignore the queue
+ // because HostSingletons mount with child fibers even if they have a single
+ // Text only child and we do not update attributes on hydrated nodes
+ prepareToHydrateHostInstance(workInProgress, currentHostContext);
+ } else {
+ workInProgress.stateNode = resolveSingletonInstance(
+ type,
+ newProps,
+ rootContainerInstance,
+ currentHostContext,
+ true,
+ );
+ markUpdate(workInProgress);
+ }
+
+ if (workInProgress.ref !== null) {
+ // If there is a ref on a host node we need to schedule a callback
+ markRef(workInProgress);
+ }
+ }
+ bubbleProperties(workInProgress);
+ return null;
+ }
+ }
+ // eslint-disable-next-line-no-fallthrough
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
@@ -1032,9 +1095,7 @@ function completeWork(
currentHostContext,
workInProgress,
);
-
appendAllChildren(instance, workInProgress, false, false);
-
workInProgress.stateNode = instance;
// Certain renderers require commit-time effects for initial mount.
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index be5ff0d1ab3a8..e22b05118f285 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -33,6 +33,7 @@ import type {Cache} from './ReactFiberCacheComponent.old';
import {
enableSuspenseAvoidThisFallback,
enableLegacyHidden,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
@@ -46,6 +47,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
HostPortal,
ContextProvider,
@@ -88,12 +90,14 @@ import {
import {
createInstance,
createTextInstance,
+ resolveSingletonInstance,
appendInitialChild,
finalizeInitialChildren,
prepareUpdate,
supportsMutation,
supportsPersistence,
supportsResources,
+ supportsSingletons,
cloneInstance,
cloneHiddenInstance,
cloneHiddenTextInstance,
@@ -227,10 +231,16 @@ if (supportsMutation) {
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
- } else if (node.tag === HostPortal) {
+ } else if (
+ node.tag === HostPortal ||
+ (enableHostSingletons && supportsSingletons
+ ? node.tag === HostSingleton
+ : false)
+ ) {
// If we have a portal child, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
+ // If we have a HostSingleton it will be placed independently
} else if (node.child !== null) {
node.child.return = node;
node = node.child;
@@ -984,6 +994,59 @@ function completeWork(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton: {
+ if (enableHostSingletons && supportsSingletons) {
+ popHostContext(workInProgress);
+ const rootContainerInstance = getRootHostContainer();
+ const type = workInProgress.type;
+ if (current !== null && workInProgress.stateNode != null) {
+ updateHostComponent(current, workInProgress, type, newProps);
+
+ if (current.ref !== workInProgress.ref) {
+ markRef(workInProgress);
+ }
+ } else {
+ if (!newProps) {
+ if (workInProgress.stateNode === null) {
+ throw new Error(
+ 'We must have new props for new mounts. This error is likely ' +
+ 'caused by a bug in React. Please file an issue.',
+ );
+ }
+
+ // This can happen when we abort work.
+ bubbleProperties(workInProgress);
+ return null;
+ }
+
+ const currentHostContext = getHostContext();
+ const wasHydrated = popHydrationState(workInProgress);
+ if (wasHydrated) {
+ // This method returns an updateQueue. we intentionally ignore the queue
+ // because HostSingletons mount with child fibers even if they have a single
+ // Text only child and we do not update attributes on hydrated nodes
+ prepareToHydrateHostInstance(workInProgress, currentHostContext);
+ } else {
+ workInProgress.stateNode = resolveSingletonInstance(
+ type,
+ newProps,
+ rootContainerInstance,
+ currentHostContext,
+ true,
+ );
+ markUpdate(workInProgress);
+ }
+
+ if (workInProgress.ref !== null) {
+ // If there is a ref on a host node we need to schedule a callback
+ markRef(workInProgress);
+ }
+ }
+ bubbleProperties(workInProgress);
+ return null;
+ }
+ }
+ // eslint-disable-next-line-no-fallthrough
case HostComponent: {
popHostContext(workInProgress);
const type = workInProgress.type;
@@ -1032,9 +1095,7 @@ function completeWork(
currentHostContext,
workInProgress,
);
-
appendAllChildren(instance, workInProgress, false, false);
-
workInProgress.stateNode = instance;
// Certain renderers require commit-time effects for initial mount.
diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js
index 354aca57160e9..ea16c1c560895 100644
--- a/packages/react-reconciler/src/ReactFiberComponentStack.js
+++ b/packages/react-reconciler/src/ReactFiberComponentStack.js
@@ -12,6 +12,7 @@ import type {Fiber} from './ReactInternalTypes';
import {
HostComponent,
HostResource,
+ HostSingleton,
LazyComponent,
SuspenseComponent,
SuspenseListComponent,
@@ -36,6 +37,7 @@ function describeFiber(fiber: Fiber): string {
const source = __DEV__ ? fiber._debugSource : null;
switch (fiber.tag) {
case HostResource:
+ case HostSingleton:
case HostComponent:
return describeBuiltInComponentFrame(fiber.type, source, owner);
case LazyComponent:
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoSingletons.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoSingletons.js
new file mode 100644
index 0000000000000..73fbb1d757fa3
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoSingletons.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+// Renderers that don't support mutation
+// can re-export everything from this module.
+
+function shim(...args: any) {
+ throw new Error(
+ 'The current renderer does not support Singletons. ' +
+ 'This error is likely caused by a bug in React. ' +
+ 'Please file an issue.',
+ );
+}
+
+// Resources (when unsupported)
+export const supportsSingletons = false;
+export const acquireSingletonInstance = shim;
+export const resolveSingletonInstance = shim;
+export const isHostSingletonType = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.new.js b/packages/react-reconciler/src/ReactFiberHotReloading.new.js
index 32ddb28afa365..fe3fe105542e8 100644
--- a/packages/react-reconciler/src/ReactFiberHotReloading.new.js
+++ b/packages/react-reconciler/src/ReactFiberHotReloading.new.js
@@ -23,6 +23,7 @@ import type {
ScheduleRoot,
} from './ReactFiberHotReloading';
+import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
import {
flushSync,
scheduleUpdateOnFiber,
@@ -38,6 +39,7 @@ import {
ForwardRef,
HostComponent,
HostResource,
+ HostSingleton,
HostPortal,
HostRoot,
MemoComponent,
@@ -48,7 +50,7 @@ import {
REACT_MEMO_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
-import {enableFloat} from 'shared/ReactFeatureFlags';
+import {supportsSingletons} from './ReactFiberHostConfig';
let resolveFamily: RefreshHandler | null = null;
let failedBoundaries: WeakSet | null = null;
@@ -425,6 +427,7 @@ function findHostInstancesForFiberShallowly(
let node = fiber;
while (true) {
switch (node.tag) {
+ case HostSingleton:
case HostComponent:
hostInstances.add(node.stateNode);
return;
@@ -453,7 +456,10 @@ function findChildHostInstancesForFiberShallowly(
while (true) {
if (
node.tag === HostComponent ||
- (enableFloat ? node.tag === HostResource : false)
+ (enableFloat ? node.tag === HostResource : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? node.tag === HostSingleton
+ : false)
) {
// We got a match.
foundHostInstances = true;
diff --git a/packages/react-reconciler/src/ReactFiberHotReloading.old.js b/packages/react-reconciler/src/ReactFiberHotReloading.old.js
index 0b553eaad3162..b5172f9a4f2b6 100644
--- a/packages/react-reconciler/src/ReactFiberHotReloading.old.js
+++ b/packages/react-reconciler/src/ReactFiberHotReloading.old.js
@@ -23,6 +23,7 @@ import type {
ScheduleRoot,
} from './ReactFiberHotReloading';
+import {enableHostSingletons, enableFloat} from 'shared/ReactFeatureFlags';
import {
flushSync,
scheduleUpdateOnFiber,
@@ -38,6 +39,7 @@ import {
ForwardRef,
HostComponent,
HostResource,
+ HostSingleton,
HostPortal,
HostRoot,
MemoComponent,
@@ -48,7 +50,7 @@ import {
REACT_MEMO_TYPE,
REACT_LAZY_TYPE,
} from 'shared/ReactSymbols';
-import {enableFloat} from 'shared/ReactFeatureFlags';
+import {supportsSingletons} from './ReactFiberHostConfig';
let resolveFamily: RefreshHandler | null = null;
let failedBoundaries: WeakSet | null = null;
@@ -425,6 +427,7 @@ function findHostInstancesForFiberShallowly(
let node = fiber;
while (true) {
switch (node.tag) {
+ case HostSingleton:
case HostComponent:
hostInstances.add(node.stateNode);
return;
@@ -453,7 +456,10 @@ function findChildHostInstancesForFiberShallowly(
while (true) {
if (
node.tag === HostComponent ||
- (enableFloat ? node.tag === HostResource : false)
+ (enableFloat ? node.tag === HostResource : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? node.tag === HostSingleton
+ : false)
) {
// We got a match.
foundHostInstances = true;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
index 82df8620e0fcb..8c25c6ab22091 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.new.js
@@ -23,6 +23,7 @@ import type {CapturedValue} from './ReactCapturedValue';
import {
HostComponent,
+ HostSingleton,
HostText,
HostRoot,
SuspenseComponent,
@@ -34,6 +35,7 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -42,6 +44,7 @@ import {
import {
shouldSetTextContent,
supportsHydration,
+ supportsSingletons,
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
@@ -68,6 +71,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
+ resolveSingletonInstance,
} from './ReactFiberHostConfig';
import {OffscreenLane} from './ReactFiberLane.new';
import {
@@ -75,6 +79,10 @@ import {
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.new';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.new';
+import {
+ getRootHostContainer,
+ getHostContext,
+} from './ReactFiberHostContext.new';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -162,6 +170,7 @@ function warnUnhydratedInstance(
);
break;
}
+ case HostSingleton:
case HostComponent: {
const isConcurrentMode = (returnFiber.mode & ConcurrentMode) !== NoMode;
didNotHydrateInstance(
@@ -218,6 +227,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
case HostRoot: {
const parentContainer = returnFiber.stateNode.containerInfo;
switch (fiber.tag) {
+ case HostSingleton:
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
@@ -242,11 +252,13 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
}
break;
}
+ case HostSingleton:
case HostComponent: {
const parentType = returnFiber.type;
const parentProps = returnFiber.memoizedProps;
const parentInstance = returnFiber.stateNode;
switch (fiber.tag) {
+ case HostSingleton:
case HostComponent: {
const type = fiber.type;
const props = fiber.pendingProps;
@@ -293,6 +305,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
const parentInstance = suspenseState.dehydrated;
if (parentInstance !== null)
switch (fiber.tag) {
+ case HostSingleton:
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
@@ -329,6 +342,8 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
function tryHydrate(fiber, nextInstance) {
switch (fiber.tag) {
+ // HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
+ // you can find it inlined in claimHydratableSingleton
case HostComponent: {
const type = fiber.type;
const props = fiber.pendingProps;
@@ -400,6 +415,25 @@ function throwOnHydrationMismatch(fiber: Fiber) {
);
}
+function claimHydratableSingleton(fiber: Fiber): void {
+ if (enableHostSingletons && supportsSingletons) {
+ if (!isHydrating) {
+ return;
+ }
+ const currentRootContainer = getRootHostContainer();
+ const currentHostContext = getHostContext();
+ const instance = (fiber.stateNode = resolveSingletonInstance(
+ fiber.type,
+ fiber.pendingProps,
+ currentRootContainer,
+ currentHostContext,
+ false,
+ ));
+ hydrationParentFiber = fiber;
+ nextHydratableInstance = getFirstHydratableChild(instance);
+ }
+}
+
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
@@ -510,6 +544,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
);
break;
}
+ case HostSingleton:
case HostComponent: {
const parentType = returnFiber.type;
const parentProps = returnFiber.memoizedProps;
@@ -585,7 +620,10 @@ function popToNextHostParent(fiber: Fiber): void {
parent !== null &&
parent.tag !== HostComponent &&
parent.tag !== HostRoot &&
- parent.tag !== SuspenseComponent
+ parent.tag !== SuspenseComponent &&
+ (!(enableHostSingletons && supportsSingletons)
+ ? true
+ : parent.tag !== HostSingleton)
) {
parent = parent.return;
}
@@ -610,16 +648,35 @@ function popHydrationState(fiber: Fiber): boolean {
return false;
}
- // If we have any remaining hydratable nodes, we need to delete them now.
- // We only do this deeper than head and body since they tend to have random
- // other nodes in them. We also ignore components with pure text content in
- // side of them. We also don't delete anything inside the root container.
- if (
- fiber.tag !== HostRoot &&
- (fiber.tag !== HostComponent ||
- (shouldDeleteUnhydratedTailInstances(fiber.type) &&
- !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
- ) {
+ let shouldClear = false;
+ if (enableHostSingletons && supportsSingletons) {
+ // With float we never clear the Root, or Singleton instances. We also do not clear Instances
+ // that have singleton text content
+ if (
+ fiber.tag !== HostRoot &&
+ fiber.tag !== HostSingleton &&
+ !(
+ fiber.tag === HostComponent &&
+ shouldSetTextContent(fiber.type, fiber.memoizedProps)
+ )
+ ) {
+ shouldClear = true;
+ }
+ } else {
+ // If we have any remaining hydratable nodes, we need to delete them now.
+ // We only do this deeper than head and body since they tend to have random
+ // other nodes in them. We also ignore components with pure text content in
+ // side of them. We also don't delete anything inside the root container.
+ if (
+ fiber.tag !== HostRoot &&
+ (fiber.tag !== HostComponent ||
+ (shouldDeleteUnhydratedTailInstances(fiber.type) &&
+ !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
+ ) {
+ shouldClear = true;
+ }
+ }
+ if (shouldClear) {
let nextInstance = nextHydratableInstance;
if (nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@@ -695,6 +752,7 @@ export {
getIsHydrating,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
+ claimHydratableSingleton,
tryToClaimNextHydratableInstance,
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
index 3f6ade7832a59..c4d567da03ab4 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.old.js
@@ -23,6 +23,7 @@ import type {CapturedValue} from './ReactCapturedValue';
import {
HostComponent,
+ HostSingleton,
HostText,
HostRoot,
SuspenseComponent,
@@ -34,6 +35,7 @@ import {
NoFlags,
DidCapture,
} from './ReactFiberFlags';
+import {enableHostSingletons} from 'shared/ReactFeatureFlags';
import {
createFiberFromHostInstanceForDeletion,
@@ -42,6 +44,7 @@ import {
import {
shouldSetTextContent,
supportsHydration,
+ supportsSingletons,
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
@@ -68,6 +71,7 @@ import {
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
+ resolveSingletonInstance,
} from './ReactFiberHostConfig';
import {OffscreenLane} from './ReactFiberLane.old';
import {
@@ -75,6 +79,10 @@ import {
restoreSuspendedTreeContext,
} from './ReactFiberTreeContext.old';
import {queueRecoverableErrors} from './ReactFiberWorkLoop.old';
+import {
+ getRootHostContainer,
+ getHostContext,
+} from './ReactFiberHostContext.old';
// The deepest Fiber on the stack involved in a hydration context.
// This may have been an insertion or a hydration.
@@ -162,6 +170,7 @@ function warnUnhydratedInstance(
);
break;
}
+ case HostSingleton:
case HostComponent: {
const isConcurrentMode = (returnFiber.mode & ConcurrentMode) !== NoMode;
didNotHydrateInstance(
@@ -218,6 +227,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
case HostRoot: {
const parentContainer = returnFiber.stateNode.containerInfo;
switch (fiber.tag) {
+ case HostSingleton:
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
@@ -242,11 +252,13 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
}
break;
}
+ case HostSingleton:
case HostComponent: {
const parentType = returnFiber.type;
const parentProps = returnFiber.memoizedProps;
const parentInstance = returnFiber.stateNode;
switch (fiber.tag) {
+ case HostSingleton:
case HostComponent: {
const type = fiber.type;
const props = fiber.pendingProps;
@@ -293,6 +305,7 @@ function warnNonhydratedInstance(returnFiber: Fiber, fiber: Fiber) {
const parentInstance = suspenseState.dehydrated;
if (parentInstance !== null)
switch (fiber.tag) {
+ case HostSingleton:
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
@@ -329,6 +342,8 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
function tryHydrate(fiber, nextInstance) {
switch (fiber.tag) {
+ // HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
+ // you can find it inlined in claimHydratableSingleton
case HostComponent: {
const type = fiber.type;
const props = fiber.pendingProps;
@@ -400,6 +415,25 @@ function throwOnHydrationMismatch(fiber: Fiber) {
);
}
+function claimHydratableSingleton(fiber: Fiber): void {
+ if (enableHostSingletons && supportsSingletons) {
+ if (!isHydrating) {
+ return;
+ }
+ const currentRootContainer = getRootHostContainer();
+ const currentHostContext = getHostContext();
+ const instance = (fiber.stateNode = resolveSingletonInstance(
+ fiber.type,
+ fiber.pendingProps,
+ currentRootContainer,
+ currentHostContext,
+ false,
+ ));
+ hydrationParentFiber = fiber;
+ nextHydratableInstance = getFirstHydratableChild(instance);
+ }
+}
+
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
@@ -510,6 +544,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): boolean {
);
break;
}
+ case HostSingleton:
case HostComponent: {
const parentType = returnFiber.type;
const parentProps = returnFiber.memoizedProps;
@@ -585,7 +620,10 @@ function popToNextHostParent(fiber: Fiber): void {
parent !== null &&
parent.tag !== HostComponent &&
parent.tag !== HostRoot &&
- parent.tag !== SuspenseComponent
+ parent.tag !== SuspenseComponent &&
+ (!(enableHostSingletons && supportsSingletons)
+ ? true
+ : parent.tag !== HostSingleton)
) {
parent = parent.return;
}
@@ -610,16 +648,35 @@ function popHydrationState(fiber: Fiber): boolean {
return false;
}
- // If we have any remaining hydratable nodes, we need to delete them now.
- // We only do this deeper than head and body since they tend to have random
- // other nodes in them. We also ignore components with pure text content in
- // side of them. We also don't delete anything inside the root container.
- if (
- fiber.tag !== HostRoot &&
- (fiber.tag !== HostComponent ||
- (shouldDeleteUnhydratedTailInstances(fiber.type) &&
- !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
- ) {
+ let shouldClear = false;
+ if (enableHostSingletons && supportsSingletons) {
+ // With float we never clear the Root, or Singleton instances. We also do not clear Instances
+ // that have singleton text content
+ if (
+ fiber.tag !== HostRoot &&
+ fiber.tag !== HostSingleton &&
+ !(
+ fiber.tag === HostComponent &&
+ shouldSetTextContent(fiber.type, fiber.memoizedProps)
+ )
+ ) {
+ shouldClear = true;
+ }
+ } else {
+ // If we have any remaining hydratable nodes, we need to delete them now.
+ // We only do this deeper than head and body since they tend to have random
+ // other nodes in them. We also ignore components with pure text content in
+ // side of them. We also don't delete anything inside the root container.
+ if (
+ fiber.tag !== HostRoot &&
+ (fiber.tag !== HostComponent ||
+ (shouldDeleteUnhydratedTailInstances(fiber.type) &&
+ !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
+ ) {
+ shouldClear = true;
+ }
+ }
+ if (shouldClear) {
let nextInstance = nextHydratableInstance;
if (nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@@ -695,6 +752,7 @@ export {
getIsHydrating,
reenterHydrationStateFromDehydratedSuspenseInstance,
resetHydrationState,
+ claimHydratableSingleton,
tryToClaimNextHydratableInstance,
prepareToHydrateHostInstance,
prepareToHydrateHostTextInstance,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index 08ded0097bbbf..1da000212bdc7 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -32,6 +32,7 @@ import {
import {get as getInstance} from 'shared/ReactInstanceMap';
import {
HostComponent,
+ HostSingleton,
ClassComponent,
HostRoot,
SuspenseComponent,
@@ -405,6 +406,7 @@ export function getPublicRootInstance(
return null;
}
switch (containerFiber.child.tag) {
+ case HostSingleton:
case HostComponent:
return getPublicInstance(containerFiber.child.stateNode);
default:
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index d71bf1c04baaa..ba046b1d197a5 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -32,6 +32,7 @@ import {
import {get as getInstance} from 'shared/ReactInstanceMap';
import {
HostComponent,
+ HostSingleton,
ClassComponent,
HostRoot,
SuspenseComponent,
@@ -405,6 +406,7 @@ export function getPublicRootInstance(
return null;
}
switch (containerFiber.child.tag) {
+ case HostSingleton:
case HostComponent:
return getPublicInstance(containerFiber.child.stateNode);
default:
diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js
index da60614dfc6fb..4f20404919350 100644
--- a/packages/react-reconciler/src/ReactFiberTreeReflection.js
+++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js
@@ -18,13 +18,15 @@ import {
ClassComponent,
HostComponent,
HostResource,
+ HostSingleton,
HostRoot,
HostPortal,
HostText,
SuspenseComponent,
} from './ReactWorkTags';
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
-import {enableFloat} from 'shared/ReactFeatureFlags';
+import {supportsResources, supportsSingletons} from './ReactFiberHostConfig';
+import {enableFloat, enableHostSingletons} from 'shared/ReactFeatureFlags';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -276,10 +278,14 @@ export function findCurrentHostFiber(parent: Fiber): Fiber | null {
function findCurrentHostFiberImpl(node: Fiber) {
// Next we'll drill down this component to find the first HostComponent/Text.
+ const tag = node.tag;
if (
- node.tag === HostComponent ||
- node.tag === HostText ||
- (enableFloat ? node.tag === HostResource : false)
+ tag === HostComponent ||
+ (enableFloat && supportsResources ? tag === HostResource : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? tag === HostSingleton
+ : false) ||
+ tag === HostText
) {
return node;
}
@@ -305,10 +311,14 @@ export function findCurrentHostFiberWithNoPortals(parent: Fiber): Fiber | null {
function findCurrentHostFiberWithNoPortalsImpl(node: Fiber) {
// Next we'll drill down this component to find the first HostComponent/Text.
+ const tag = node.tag;
if (
- node.tag === HostComponent ||
- node.tag === HostText ||
- (enableFloat ? node.tag === HostResource : false)
+ tag === HostComponent ||
+ (enableFloat && supportsResources ? tag === HostResource : false) ||
+ (enableHostSingletons && supportsSingletons
+ ? tag === HostSingleton
+ : false) ||
+ tag === HostText
) {
return node;
}
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js
index 66794fbccd4df..cb59bd936b33c 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js
@@ -20,6 +20,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostPortal,
ContextProvider,
SuspenseComponent,
@@ -117,6 +118,7 @@ function unwindWork(
return null;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
// TODO: popHydrationState
popHostContext(workInProgress);
@@ -236,6 +238,7 @@ function unwindInterruptedWork(
break;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
popHostContext(interruptedWork);
break;
diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js
index e30fa19532885..732894027629c 100644
--- a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js
@@ -20,6 +20,7 @@ import {
HostRoot,
HostComponent,
HostResource,
+ HostSingleton,
HostPortal,
ContextProvider,
SuspenseComponent,
@@ -117,6 +118,7 @@ function unwindWork(
return null;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
// TODO: popHydrationState
popHostContext(workInProgress);
@@ -236,6 +238,7 @@ function unwindInterruptedWork(
break;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
popHostContext(interruptedWork);
break;
diff --git a/packages/react-reconciler/src/ReactTestSelectors.js b/packages/react-reconciler/src/ReactTestSelectors.js
index bda36f6d2a1aa..1869fb26576ef 100644
--- a/packages/react-reconciler/src/ReactTestSelectors.js
+++ b/packages/react-reconciler/src/ReactTestSelectors.js
@@ -13,6 +13,7 @@ import type {Instance} from './ReactFiberHostConfig';
import {
HostComponent,
HostResource,
+ HostSingleton,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -142,6 +143,7 @@ function findFiberRootForHostRoot(hostRoot: Instance): Fiber {
}
function matchSelector(fiber: Fiber, selector: Selector): boolean {
+ const tag = fiber.tag;
switch (selector.$$typeof) {
case COMPONENT_TYPE:
if (fiber.type === selector.value) {
@@ -154,7 +156,11 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
((selector: any): HasPseudoClassSelector).value,
);
case ROLE_TYPE:
- if (fiber.tag === HostComponent || fiber.tag === HostResource) {
+ if (
+ tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton
+ ) {
const node = fiber.stateNode;
if (
matchAccessibilityRole(node, ((selector: any): RoleSelector).value)
@@ -165,9 +171,10 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
break;
case TEXT_TYPE:
if (
- fiber.tag === HostComponent ||
- fiber.tag === HostText ||
- fiber.tag === HostResource
+ tag === HostComponent ||
+ tag === HostText ||
+ tag === HostResource ||
+ tag === HostSingleton
) {
const textContent = getTextContent(fiber);
if (
@@ -179,7 +186,11 @@ function matchSelector(fiber: Fiber, selector: Selector): boolean {
}
break;
case TEST_NAME_TYPE:
- if (fiber.tag === HostComponent || fiber.tag === HostResource) {
+ if (
+ tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton
+ ) {
const dataTestID = fiber.memoizedProps['data-testname'];
if (
typeof dataTestID === 'string' &&
@@ -222,11 +233,14 @@ function findPaths(root: Fiber, selectors: Array): Array {
let index = 0;
while (index < stack.length) {
const fiber = ((stack[index++]: any): Fiber);
+ const tag = fiber.tag;
let selectorIndex = ((stack[index++]: any): number);
let selector = selectors[selectorIndex];
if (
- (fiber.tag === HostComponent || fiber.tag === HostResource) &&
+ (tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton) &&
isHiddenSubtree(fiber)
) {
continue;
@@ -257,11 +271,14 @@ function hasMatchingPaths(root: Fiber, selectors: Array): boolean {
let index = 0;
while (index < stack.length) {
const fiber = ((stack[index++]: any): Fiber);
+ const tag = fiber.tag;
let selectorIndex = ((stack[index++]: any): number);
let selector = selectors[selectorIndex];
if (
- (fiber.tag === HostComponent || fiber.tag === HostResource) &&
+ (tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton) &&
isHiddenSubtree(fiber)
) {
continue;
@@ -303,7 +320,12 @@ export function findAllNodes(
let index = 0;
while (index < stack.length) {
const node = ((stack[index++]: any): Fiber);
- if (node.tag === HostComponent || node.tag === HostResource) {
+ const tag = node.tag;
+ if (
+ tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton
+ ) {
if (isHiddenSubtree(node)) {
continue;
}
@@ -338,11 +360,14 @@ export function getFindAllNodesFailureDescription(
let index = 0;
while (index < stack.length) {
const fiber = ((stack[index++]: any): Fiber);
+ const tag = fiber.tag;
let selectorIndex = ((stack[index++]: any): number);
const selector = selectors[selectorIndex];
if (
- (fiber.tag === HostComponent || fiber.tag === HostResource) &&
+ (tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton) &&
isHiddenSubtree(fiber)
) {
continue;
@@ -493,10 +518,15 @@ export function focusWithin(
let index = 0;
while (index < stack.length) {
const fiber = ((stack[index++]: any): Fiber);
+ const tag = fiber.tag;
if (isHiddenSubtree(fiber)) {
continue;
}
- if (fiber.tag === HostComponent || fiber.tag === HostResource) {
+ if (
+ tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton
+ ) {
const node = fiber.stateNode;
if (setFocusIfFocusable(node)) {
return true;
diff --git a/packages/react-reconciler/src/ReactWorkTags.js b/packages/react-reconciler/src/ReactWorkTags.js
index 0a62312ce87a0..827ac1b78216d 100644
--- a/packages/react-reconciler/src/ReactWorkTags.js
+++ b/packages/react-reconciler/src/ReactWorkTags.js
@@ -34,7 +34,8 @@ export type WorkTag =
| 23
| 24
| 25
- | 26;
+ | 26
+ | 27;
export const FunctionComponent = 0;
export const ClassComponent = 1;
@@ -62,3 +63,4 @@ export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostResource = 26;
+export const HostSingleton = 27;
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index 4bb920ca8b78a..e856b042a0544 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -199,3 +199,12 @@ export const isHostResourceType = $$$hostConfig.isHostResourceType;
export const getResource = $$$hostConfig.getResource;
export const acquireResource = $$$hostConfig.acquireResource;
export const releaseResource = $$$hostConfig.releaseResource;
+
+// -------------------
+// Singletons
+// (optional)
+// -------------------
+export const supportsSingletons = $$$hostConfig.supportsSingletons;
+export const acquireSingletonInstance = $$$hostConfig.acquireSingletonInstance;
+export const resolveSingletonInstance = $$$hostConfig.resolveSingletonInstance;
+export const isHostSingletonType = $$$hostConfig.isHostSingletonType;
diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js
index 4b14f01014714..190fce588f327 100644
--- a/packages/react-reconciler/src/getComponentNameFromFiber.js
+++ b/packages/react-reconciler/src/getComponentNameFromFiber.js
@@ -20,6 +20,7 @@ import {
HostPortal,
HostComponent,
HostResource,
+ HostSingleton,
HostText,
Fragment,
Mode,
@@ -79,6 +80,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null {
case Fragment:
return 'Fragment';
case HostResource:
+ case HostSingleton:
case HostComponent:
// Host component type is the display name (e.g. "div", "View")
return type;
diff --git a/packages/react-test-renderer/src/ReactTestHostConfig.js b/packages/react-test-renderer/src/ReactTestHostConfig.js
index e8daa8368f9e7..c524bb181a9f5 100644
--- a/packages/react-test-renderer/src/ReactTestHostConfig.js
+++ b/packages/react-test-renderer/src/ReactTestHostConfig.js
@@ -47,6 +47,7 @@ export * from 'react-reconciler/src/ReactFiberHostConfigWithNoHydration';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoTestSelectors';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoMicrotasks';
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoResources';
+export * from 'react-reconciler/src/ReactFiberHostConfigWithNoSingletons';
const NO_CONTEXT = {};
const UPDATE_SIGNAL = {};
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 1bcd724eb7ec1..57996d680d0cc 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -32,6 +32,7 @@ import {
ClassComponent,
HostComponent,
HostResource,
+ HostSingleton,
HostPortal,
HostText,
HostRoot,
@@ -202,6 +203,7 @@ function toTree(node: ?Fiber) {
rendered: childrenToTree(node.child),
};
case HostResource:
+ case HostSingleton:
case HostComponent: {
return {
nodeType: 'host',
@@ -308,7 +310,12 @@ class ReactTestInstance {
}
get instance(): $FlowFixMe {
- if (this._fiber.tag === HostComponent || this._fiber.tag === HostResource) {
+ const tag = this._fiber.tag;
+ if (
+ tag === HostComponent ||
+ tag === HostResource ||
+ tag === HostSingleton
+ ) {
return getPublicInstance(this._fiber.stateNode);
} else {
return this._fiber.stateNode;
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 67a9ac2014d6e..f43beacb79d7e 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -100,6 +100,10 @@ export const enableSuspenseAvoidThisFallbackFizz = false;
export const enableCPUSuspense = __EXPERIMENTAL__;
+export const enableHostSingletons = __EXPERIMENTAL__;
+
+export const enableFloat = __EXPERIMENTAL__;
+
// When a node is unmounted, recurse into the Fiber subtree and clean out
// references. Each level cleans up more fiber fields than the previous level.
// As far as we know, React itself doesn't leak, but because the Fiber contains
@@ -113,7 +117,6 @@ export const enableCPUSuspense = __EXPERIMENTAL__;
// aggressiveness.
export const deletedTreeCleanUpLevel = 3;
-export const enableFloat = __EXPERIMENTAL__;
export const enableUseHook = __EXPERIMENTAL__;
// Enables unstable_useMemoCache hook, intended as a compilation target for
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index beafb67bf0716..e81b1d3d37252 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -84,6 +84,7 @@ export const enableUseMutableSource = true;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 3fdedac1118e1..7dc0ac1e2febc 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -73,6 +73,7 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index ce7afbcef7084..2d408c9c10f36 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -73,6 +73,7 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
index 7d9054bfeb7de..69d0c027aec52 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
@@ -71,6 +71,7 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 38278fbc93d42..9ff5908cc4e87 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -75,6 +75,7 @@ export const enableUseMutableSource = true;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 308aaed94dfaa..f2346a66aa30f 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -73,6 +73,7 @@ export const enableUseMutableSource = false;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index 89b24c8ed6d03..7114d9d21289f 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -74,6 +74,7 @@ export const enableUseMutableSource = true;
export const enableTransitionTracing = false;
export const enableFloat = false;
+export const enableHostSingletons = false;
export const useModernStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index 67812e36dae55..28f5aa531d307 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -59,6 +59,5 @@ export const disableNativeComponentFrames = false;
export const createRootStrictEffectsByDefault = false;
export const enableStrictEffects = false;
export const allowConcurrentByDefault = true;
-export const enableFloat = false;
// You probably *don't* want to add more hardcoded ones.
// Instead, try to add them above with the __VARIANT__ value.
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index 1811c9bf7ce57..755d44053c465 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -56,6 +56,7 @@ export const enableFloat = false;
export const enableUseHook = true;
export const enableUseMemoCacheHook = true;
export const enableUseEventHook = true;
+export const enableHostSingletons = false;
// Logs additional User Timing API marks for use with an experimental profiling tool.
export const enableSchedulingProfiler: boolean =
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 48fcf61717273..4e24b4d096a83 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -432,5 +432,12 @@
"444": "getResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
"445": "\"currentResources\" was expected to exist. This is a bug in React.",
"446": "\"resourceRoot\" was expected to exist. This is a bug in React.",
- "447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found."
+ "447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.",
+ "448": "\"currentDocument\" was expected to exist. This is a bug in React.",
+ "449": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found.",
+ "450": "The current renderer does not support Singletons. This error is likely caused by a bug in React. Please file an issue.",
+ "451": "resolveSingletonInstance was called with an element type that is not supported. This is a bug in React.",
+ "452": "React expected an 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.",
+ "453": "React expected a 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.",
+ "454": "React expected a 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."
}
+ ,
+ );
+ });
+
+ // @gate enableHostSingletons && enableFloat
+ it('clears persistent body when it is the container', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+