From 2cf4352e1c81a5b8c3528519a128c20e8e65531d Mon Sep 17 00:00:00 2001
From: Josh Story
Date: Tue, 11 Oct 2022 08:42:42 -0700
Subject: [PATCH] Implement HostSingleton Fiber type (#25426)
---
packages/react-art/src/ReactARTHostConfig.js | 1 +
.../src/client/ReactDOMComponent.js | 10 +-
.../src/client/ReactDOMComponentTree.js | 27 +-
.../src/client/ReactDOMHostConfig.js | 354 ++++++-
.../src/events/DOMPluginEventSystem.js | 23 +-
.../events/plugins/EnterLeaveEventPlugin.js | 12 +-
.../src/__tests__/ReactDOMFizzServer-test.js | 1 -
.../src/__tests__/ReactDOMFloat-test.js | 67 +-
.../src/__tests__/ReactDOMRoot-test.js | 23 +-
.../ReactDOMSingletonComponents-test.js | 982 ++++++++++++++++++
.../src/__tests__/ReactRenderDocument-test.js | 38 +-
.../src/__tests__/validateDOMNesting-test.js | 61 +-
packages/react-dom/src/client/ReactDOMRoot.js | 8 +-
.../DOMPluginEventSystem-test.internal.js | 11 +-
.../src/test-utils/ReactTestUtils.js | 11 +-
.../src/ReactFabricHostConfig.js | 1 +
.../src/ReactNativeHostConfig.js | 1 +
.../src/createReactNoop.js | 2 +
.../react-reconciler/src/ReactFiber.new.js | 24 +-
.../react-reconciler/src/ReactFiber.old.js | 24 +-
.../src/ReactFiberBeginWork.new.js | 48 +-
.../src/ReactFiberBeginWork.old.js | 48 +-
.../src/ReactFiberCommitWork.new.js | 111 +-
.../src/ReactFiberCommitWork.old.js | 111 +-
.../src/ReactFiberCompleteWork.new.js | 67 +-
.../src/ReactFiberCompleteWork.old.js | 67 +-
.../src/ReactFiberComponentStack.js | 2 +
.../ReactFiberHostConfigWithNoSingletons.js | 27 +
.../src/ReactFiberHotReloading.new.js | 10 +-
.../src/ReactFiberHotReloading.old.js | 10 +-
.../src/ReactFiberHydrationContext.new.js | 80 +-
.../src/ReactFiberHydrationContext.old.js | 80 +-
.../src/ReactFiberReconciler.new.js | 2 +
.../src/ReactFiberReconciler.old.js | 2 +
.../src/ReactFiberTreeReflection.js | 19 +-
.../src/ReactFiberUnwindWork.new.js | 3 +
.../src/ReactFiberUnwindWork.old.js | 3 +
.../src/ReactTestSelectors.js | 50 +-
.../react-reconciler/src/ReactWorkTags.js | 4 +-
.../src/forks/ReactFiberHostConfig.custom.js | 11 +
.../src/getComponentNameFromFiber.js | 2 +
.../src/ReactTestHostConfig.js | 1 +
.../src/ReactTestRenderer.js | 9 +-
packages/shared/ReactFeatureFlags.js | 5 +-
.../forks/ReactFeatureFlags.native-fb.js | 1 +
.../forks/ReactFeatureFlags.native-oss.js | 1 +
.../forks/ReactFeatureFlags.test-renderer.js | 1 +
.../ReactFeatureFlags.test-renderer.native.js | 1 +
.../ReactFeatureFlags.test-renderer.www.js | 1 +
.../shared/forks/ReactFeatureFlags.testing.js | 1 +
.../forks/ReactFeatureFlags.testing.www.js | 1 +
.../forks/ReactFeatureFlags.www-dynamic.js | 1 -
.../shared/forks/ReactFeatureFlags.www.js | 1 +
scripts/error-codes/codes.json | 7 +-
54 files changed, 2276 insertions(+), 193 deletions(-)
create mode 100644 packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js
create mode 100644 packages/react-reconciler/src/ReactFiberHostConfigWithNoSingletons.js
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..fdcdf9801a218 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
@@ -73,6 +73,7 @@ import {
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
enableClientRenderFallbackOnTextMismatch,
+ enableHostSingletons,
} from 'shared/ReactFeatureFlags';
import {
mediaEventTypes,
@@ -312,12 +313,17 @@ function setInitialDOMProperties(
// textContent on a
+ ,
+ );
+ expect(() => {
+ expect(Scheduler).toFlushWithoutYielding();
+ }).toErrorDev(
+ '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 and if you need to mount a new one, ensure any previous ones have unmounted first',
+ );
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ Hello
+ Hola
+
+
+ ,
+ );
+
+ root.render(
+
+ {null}
+ {null}
+
+ Bonjour
+
+
+ hello world
+
+ goodbye
+
+ ,
+ );
+ pipe(writable);
+ });
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+ a server title
+
+
+
+
+ hello world
+
+ goodbye
+
+ ,
+ );
+ const {documentElement, head, body} = document;
+ const persistentElements = [documentElement, head, body];
+
+ // Render into the document completely different html. Observe that styles
+ // are retained as are html, body, and head referential identities. Because this was
+ // server rendered and we are not hydrating we lose the semantic placement of the original
+ // head contents and everything gets preprended. In a future update we might emit an insertion
+ // edge from the server and make client rendering reslilient to interstitial placement
+ const root = ReactDOMClient.createRoot(document);
+ root.render(
+
+
+ a client title
+
+
+ hello client
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ // Similar to Hydration we don't reset attributes on the instance itself even on a fresh render.
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a client title
+
+
+
+ hello client
+
+ ,
+ );
+
+ // Render new children and assert they append in the correct locations
+ root.render(
+
+
+ a client title
+
+
+
+ hello client again
+ hello client
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a client title
+
+
+
+
+ hello client again
+ hello client
+
+ ,
+ );
+
+ // Remove some children
+ root.render(
+
+
+ a client title
+
+
+ hello client again
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a client title
+
+
+
+ hello client again
+
+ ,
+ );
+
+ // Remove a persistent component
+ // @TODO figure out whether to clean up attributes. restoring them is likely
+ // not possible.
+ root.render(
+
+
+ a client title
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a client title
+
+
+
+
+ ,
+ );
+
+ // unmount the root
+ root.unmount();
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ // Now let's hydrate the document with known mismatching content
+ // We assert that the identities of html, head, and body still haven't changed
+ // and that the embedded styles are still retained
+ const hydrationErrors = [];
+ let hydrateRoot = ReactDOMClient.hydrateRoot(
+ document,
+
+
+ a client title
+
+
+ hello client
+
+ ,
+ {
+ onRecoverableError(error, errorInfo) {
+ hydrationErrors.push([
+ error.message,
+ errorInfo.componentStack
+ ? errorInfo.componentStack.split('\n')[1].trim()
+ : null,
+ ]);
+ },
+ },
+ );
+ expect(() => {
+ expect(Scheduler).toFlushWithoutYielding();
+ }).toErrorDev(
+ [
+ `Warning: Expected server HTML to contain a matching in .
+ in title (at **)
+ in head (at **)
+ in html (at **)`,
+ `Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.`,
+ ],
+ {withoutStack: 1},
+ );
+ expect(hydrationErrors).toEqual([
+ [
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
+ 'at title',
+ ],
+ [
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
+ 'at div',
+ ],
+ [
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
+ null,
+ ],
+ ]);
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ a client title
+
+
+
+ hello client
+
+ ,
+ );
+
+ // Reset the tree
+ hydrationErrors.length = 0;
+ hydrateRoot.unmount();
+
+ // Now we try hydrating again with matching nodes and we ensure
+ // the retained styles are bound to the hydrated fibers
+ const link = document.querySelector('link[rel="stylesheet"]');
+ const style = document.querySelector('style');
+ hydrateRoot = ReactDOMClient.hydrateRoot(
+ document,
+
+
+
+
+
+
+
+
+
+ ,
+ {
+ onRecoverableError(error, errorInfo) {
+ hydrationErrors.push([
+ error.message,
+ errorInfo.componentStack
+ ? errorInfo.componentStack.split('\n')[1].trim()
+ : null,
+ ]);
+ },
+ },
+ );
+ expect(hydrationErrors).toEqual([]);
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect([link, style]).toEqual([
+ document.querySelector('link[rel="stylesheet"]'),
+ document.querySelector('style'),
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+ ,
+ );
+
+ // We unmount a final time and observe that still we retain our persistent nodes
+ // but they style contents which matched in hydration is removed
+ hydrateRoot.unmount();
+ expect(persistentElements).toEqual([
+ document.documentElement,
+ document.head,
+ document.body,
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ hello
+
+ ,
+ );
+ pipe(writable);
+ });
+ const root = ReactDOMClient.hydrateRoot(
+ document,
+
+
+ title
+
+
+ hello
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+
+ // We construct and insert some artificial stylesheets mimicing what a 3rd party script might do
+ // In the future we could hydrate with these already in the document but the rules are restrictive
+ // still so it would fail and fall back to client rendering
+ const [a, b, c, d, e, f, g, h] = 'abcdefgh'.split('').map(letter => {
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = letter;
+ return link;
+ });
+
+ const head = document.head;
+ const title = head.firstChild;
+ head.insertBefore(a, title);
+ head.insertBefore(b, title);
+ head.appendChild(c);
+ head.appendChild(d);
+
+ const bodyContent = document.body.firstChild;
+ const body = document.body;
+ body.insertBefore(e, bodyContent);
+ body.insertBefore(f, bodyContent);
+ body.appendChild(g);
+ body.appendChild(h);
+
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ title
+
+
+
+
+
+
+ hello
+
+
+
+ ,
+ );
+
+ // Unmount head and change children of body
+ root.render(
+
+ {null}
+
+ hello
+ world
+
+ ,
+ );
+
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
+ hello
+ world
+
+
+
+ ,
+ );
+
+ // Mount new head and unmount body
+ root.render(
+
+
+ a new title
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ a new title
+
+
+
+
+
+
+
+
+
+
+
+ ,
+ );
+ });
+
+ // @gate enableHostSingletons
+ it('clears persistent head and body when html is the container', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+
+
+ this should be removed
+
+
+
+
+ this should be removed
+
+
+ ,
+ );
+ pipe(writable);
+ });
+ container = document.documentElement;
+
+ const root = ReactDOMClient.createRoot(container);
+ root.render(
+ <>
+
+ something new
+
+
+ something new
+
+ >,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ something new
+
+
+
+
+ something new
+
+ ,
+ );
+ });
+
+ // @gate enableHostSingletons
+ it('clears persistent head when it is the container', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+
+
+ this should be removed
+
+
+
+
+ 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
+
+ ,
+ );
+ });
+
+ 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
+ ,
+ );
+ });
+
+ it('supports going from single text child to many children back to single text child in body', async () => {
+ const root = ReactDOMClient.createRoot(document);
+ root.render(
+
+ foo
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+ foo
+ ,
+ );
+
+ root.render(
+
+
+ foo
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ foo
+
+ ,
+ );
+
+ root.render(
+
+ foo
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+ foo
+ ,
+ );
+
+ root.render(
+
+
+ foo
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ foo
+
+ ,
+ );
+ });
+});
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/__tests__/validateDOMNesting-test.js b/packages/react-dom/src/__tests__/validateDOMNesting-test.js
index 1f67cc1f571dc..cdc97fbef3df1 100644
--- a/packages/react-dom/src/__tests__/validateDOMNesting-test.js
+++ b/packages/react-dom/src/__tests__/validateDOMNesting-test.js
@@ -102,22 +102,49 @@ describe('validateDOMNesting', () => {
' in html (at **)',
],
);
- expectWarnings(
- ['body', 'body'],
- [
- 'render(): Rendering components directly into document.body is discouraged',
- 'validateDOMNesting(...): cannot appear as a child of .\n' +
- ' in body (at **)',
- ],
- 1,
- );
- expectWarnings(
- ['svg', 'foreignObject', 'body', 'p'],
- [
- 'validateDOMNesting(...): cannot appear as a child of .\n' +
- ' in body (at **)\n' +
- ' in foreignObject (at **)',
- ],
- );
+ if (gate(flags => flags.enableHostSingletons)) {
+ expectWarnings(
+ ['body', 'body'],
+ [
+ 'render(): Rendering components directly into document.body is discouraged',
+ 'validateDOMNesting(...): cannot appear as a child of .\n' +
+ ' in body (at **)',
+ '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 and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
+ ' in body (at **)',
+ ],
+ 1,
+ );
+ } else {
+ expectWarnings(
+ ['body', 'body'],
+ [
+ 'render(): Rendering components directly into document.body is discouraged',
+ 'validateDOMNesting(...): cannot appear as a child of .\n' +
+ ' in body (at **)',
+ ],
+ 1,
+ );
+ }
+ if (gate(flags => flags.enableHostSingletons)) {
+ expectWarnings(
+ ['svg', 'foreignObject', 'body', 'p'],
+ [
+ 'validateDOMNesting(...): cannot appear as a child of .\n' +
+ ' in body (at **)\n' +
+ ' in foreignObject (at **)',
+ '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 and if you need to mount a new one, ensure any previous ones have unmounted first.\n' +
+ ' in body (at **)',
+ ],
+ );
+ } else {
+ expectWarnings(
+ ['svg', 'foreignObject', 'body', 'p'],
+ [
+ 'validateDOMNesting(...): cannot appear as a child of .\n' +
+ ' in body (at **)\n' +
+ ' in foreignObject (at **)',
+ ],
+ );
+ }
});
});
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..66c679b424646 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 && !getIsHydrating()) {
+ // 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..e5c3f1f4cb562 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 && !getIsHydrating()) {
+ // 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..343cc152d735c 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,9 @@ import {
detachDeletedInstance,
acquireResource,
releaseResource,
+ clearSingleton,
+ acquireSingletonInstance,
+ releaseSingletonInstance,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -501,6 +507,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
}
case HostComponent:
case HostResource:
+ case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
@@ -1053,6 +1060,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 +1106,7 @@ function commitLayoutEffectOnFiber(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton:
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
@@ -1487,7 +1496,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 +1575,7 @@ function commitAttachRef(finishedWork: Fiber) {
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
+ case HostSingleton:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
@@ -1765,8 +1780,11 @@ function isHostParent(fiber: Fiber): boolean {
return (
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) ||
+ fiber.tag === HostPortal
);
}
@@ -1792,6 +1810,9 @@ function getHostSibling(fiber: Fiber): ?Instance {
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
+ (!(enableHostSingletons && supportsSingletons)
+ ? true
+ : node.tag !== HostSingleton) &&
node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
@@ -1822,11 +1843,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 +1911,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 +1946,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 +2001,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 +2067,41 @@ 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);
+ // Normally this is called in passive unmount effect phase however with
+ // HostSingleton we warn if you acquire one that is already associated to
+ // a different fiber. To increase our chances of avoiding this, specifically
+ // if you keyed a HostSingleton so there will be a delete followed by a Placement
+ // we treat detach eagerly here
+ releaseSingletonInstance(deletedFiber.stateNode);
+
+ hostParent = prevHostParent;
+ hostParentIsContainer = prevHostParentIsContainer;
+
return;
}
}
@@ -2543,6 +2618,26 @@ 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;
+ const props = finishedWork.memoizedProps;
+ // This was a new mount, we need to clear and set initial properties
+ clearSingleton(singleton);
+ acquireSingletonInstance(
+ finishedWork.type,
+ props,
+ singleton,
+ finishedWork,
+ );
+ }
+ }
+ }
+ }
+ // eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
@@ -2935,6 +3030,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
break;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3035,6 +3131,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..0dac9e775a7fb 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,9 @@ import {
detachDeletedInstance,
acquireResource,
releaseResource,
+ clearSingleton,
+ acquireSingletonInstance,
+ releaseSingletonInstance,
} from './ReactFiberHostConfig';
import {
captureCommitPhaseError,
@@ -501,6 +507,7 @@ function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
}
case HostComponent:
case HostResource:
+ case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
@@ -1053,6 +1060,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 +1106,7 @@ function commitLayoutEffectOnFiber(
}
}
// eslint-disable-next-line-no-fallthrough
+ case HostSingleton:
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
@@ -1487,7 +1496,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 +1575,7 @@ function commitAttachRef(finishedWork: Fiber) {
let instanceToUse;
switch (finishedWork.tag) {
case HostResource:
+ case HostSingleton:
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
@@ -1765,8 +1780,11 @@ function isHostParent(fiber: Fiber): boolean {
return (
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) ||
+ fiber.tag === HostPortal
);
}
@@ -1792,6 +1810,9 @@ function getHostSibling(fiber: Fiber): ?Instance {
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
+ (!(enableHostSingletons && supportsSingletons)
+ ? true
+ : node.tag !== HostSingleton) &&
node.tag !== DehydratedFragment
) {
// If it is not host node and, we might have a host node inside it.
@@ -1822,11 +1843,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 +1911,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 +1946,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 +2001,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 +2067,41 @@ 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);
+ // Normally this is called in passive unmount effect phase however with
+ // HostSingleton we warn if you acquire one that is already associated to
+ // a different fiber. To increase our chances of avoiding this, specifically
+ // if you keyed a HostSingleton so there will be a delete followed by a Placement
+ // we treat detach eagerly here
+ releaseSingletonInstance(deletedFiber.stateNode);
+
+ hostParent = prevHostParent;
+ hostParentIsContainer = prevHostParentIsContainer;
+
return;
}
}
@@ -2543,6 +2618,26 @@ 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;
+ const props = finishedWork.memoizedProps;
+ // This was a new mount, we need to clear and set initial properties
+ clearSingleton(singleton);
+ acquireSingletonInstance(
+ finishedWork.type,
+ props,
+ singleton,
+ finishedWork,
+ );
+ }
+ }
+ }
+ }
+ // eslint-disable-next-line-no-fallthrough
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
@@ -2935,6 +3030,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
break;
}
case HostResource:
+ case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3035,6 +3131,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..195a6f8d54ffc 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) {
+ // We ignore the boolean indicating there is an updateQueue because
+ // it is used only to set text children and HostSingletons do not
+ // use them.
+ 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..1ead0a8902311 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) {
+ // We ignore the boolean indicating there is an updateQueue because
+ // it is used only to set text children and HostSingletons do not
+ // use them.
+ 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..a20af2a2cd913
--- /dev/null
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoSingletons.js
@@ -0,0 +1,27 @@
+/**
+ * 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 resolveSingletonInstance = shim;
+export const clearSingleton = shim;
+export const acquireSingletonInstance = shim;
+export const releaseSingletonInstance = 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..cfaa44da6d39b 100644
--- a/packages/react-reconciler/src/ReactFiberTreeReflection.js
+++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js
@@ -18,13 +18,14 @@ import {
ClassComponent,
HostComponent,
HostResource,
+ HostSingleton,
HostRoot,
HostPortal,
HostText,
SuspenseComponent,
} from './ReactWorkTags';
import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
-import {enableFloat} from 'shared/ReactFeatureFlags';
+import {enableFloat, enableHostSingletons} from 'shared/ReactFeatureFlags';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -276,10 +277,12 @@ 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 ? tag === HostResource : false) ||
+ (enableHostSingletons ? tag === HostSingleton : false) ||
+ tag === HostText
) {
return node;
}
@@ -305,10 +308,12 @@ 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 ? tag === HostResource : false) ||
+ (enableHostSingletons ? 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..9c1499acefbfe 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -199,3 +199,14 @@ 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 resolveSingletonInstance = $$$hostConfig.resolveSingletonInstance;
+export const clearSingleton = $$$hostConfig.clearSingleton;
+export const acquireSingletonInstance = $$$hostConfig.acquireSingletonInstance;
+export const releaseSingletonInstance = $$$hostConfig.releaseSingletonInstance;
+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 9995cc0456835..db795a516fe49 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -434,5 +434,10 @@
"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.",
"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.",
- "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."
+ "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.",
+ "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."
}
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ ,
+ );
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ ,
+ );
+ });
+
+ // @gate enableHostSingletons
+ it('renders into html, head, and body persistently so the node identities never change and extraneous styles are retained', async () => {
+ gate(flags => {
+ if (flags.enableHostSingletons !== true) {
+ // We throw here because when this test fails it ends up with sync work in a microtask
+ // that throws after the expectTestToFail check asserts the failure. this causes even the
+ // expected failure to fail. This just fails explicitly and early
+ throw new Error('manually opting out of test');
+ }
+ });
+ // Server render some html that will get replaced with a client render
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+
+ ,
+ );
+ });
+
+ // This test is not supported in this implementation. If we reintroduce insertion edge we should revisit
+ // @gate enableHostSingletons
+ xit('is able to maintain insertions in head and body between tree-adjacent Nodes', async () => {
+ // Server render some html and hydrate on the client
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+
+ ,
+ );
+ pipe(writable);
+ });
+ container = document.head;
+
+ const root = ReactDOMClient.createRoot(container);
+ root.render(
);
+ expect(Scheduler).toFlushWithoutYielding();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+ ,
+ );
+ });
+
+ // @gate enableHostSingletons && enableFloat
+ it('clears persistent body when it is the container', async () => {
+ await actIntoEmptyDocument(() => {
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
+
+