diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index cea03b73b59d5..042d6897c6aae 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -798,24 +798,37 @@ export function appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
- let parentNode;
- if (container.nodeType === COMMENT_NODE) {
- parentNode = (container.parentNode: any);
- if (supportsMoveBefore) {
- // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
- parentNode.moveBefore(child, container);
- } else {
- parentNode.insertBefore(child, container);
+ let parentNode: Document | Element;
+ switch (container.nodeType) {
+ case COMMENT_NODE: {
+ parentNode = (container.parentNode: any);
+ if (supportsMoveBefore) {
+ // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
+ parentNode.moveBefore(child, container);
+ } else {
+ parentNode.insertBefore(child, container);
+ }
+ return;
}
- } else {
- parentNode = container;
- if (supportsMoveBefore) {
- // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
- parentNode.moveBefore(child, null);
- } else {
- parentNode.appendChild(child);
+ case DOCUMENT_NODE: {
+ parentNode = (container: any).body;
+ break;
}
+ default: {
+ if (container.nodeName === 'HTML') {
+ parentNode = (container.ownerDocument.body: any);
+ } else {
+ parentNode = (container: any);
+ }
+ }
+ }
+ if (supportsMoveBefore) {
+ // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
+ parentNode.moveBefore(child, null);
+ } else {
+ parentNode.appendChild(child);
}
+
// This container might be used for a portal.
// If something inside a portal is clicked, that click should bubble
// through the React tree. However, on Mobile Safari the click would
@@ -852,21 +865,35 @@ export function insertInContainerBefore(
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
- if (container.nodeType === COMMENT_NODE) {
- if (supportsMoveBefore) {
- // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
- (container.parentNode: any).moveBefore(child, beforeChild);
- } else {
- (container.parentNode: any).insertBefore(child, beforeChild);
+ let parentNode: Document | Element;
+ switch (container.nodeType) {
+ case COMMENT_NODE: {
+ parentNode = (container.parentNode: any);
+ break;
}
- } else {
- if (supportsMoveBefore) {
- // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
- container.moveBefore(child, beforeChild);
- } else {
- container.insertBefore(child, beforeChild);
+ case DOCUMENT_NODE: {
+ const ownerDocument: Document = (container: any);
+ parentNode = (ownerDocument.body: any);
+ break;
+ }
+ default: {
+ if (container.nodeName === 'HTML') {
+ parentNode = (container.ownerDocument.body: any);
+ } else {
+ parentNode = (container: any);
+ }
}
}
+ if (supportsMoveBefore) {
+ // $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
+ parentNode.moveBefore(child, beforeChild);
+ } else {
+ parentNode.insertBefore(child, beforeChild);
+ }
+}
+
+export function isSingletonScope(type: string): boolean {
+ return type === 'head';
}
function createEvent(type: DOMEventName, bubbles: boolean): Event {
@@ -912,11 +939,22 @@ export function removeChildFromContainer(
container: Container,
child: Instance | TextInstance | SuspenseInstance,
): void {
- if (container.nodeType === COMMENT_NODE) {
- (container.parentNode: any).removeChild(child);
- } else {
- container.removeChild(child);
+ let parentNode: Document | Element;
+ switch (container.nodeType) {
+ case COMMENT_NODE:
+ parentNode = (container.parentNode: any);
+ break;
+ case DOCUMENT_NODE:
+ parentNode = (container: any).body;
+ break;
+ default:
+ if (container.nodeName === 'HTML') {
+ parentNode = (container.ownerDocument.body: any);
+ } else {
+ parentNode = (container: any);
+ }
}
+ parentNode.removeChild(child);
}
export function clearSuspenseBoundary(
@@ -964,10 +1002,15 @@ export function clearSuspenseBoundaryFromContainer(
): void {
if (container.nodeType === COMMENT_NODE) {
clearSuspenseBoundary((container.parentNode: any), suspenseInstance);
- } else if (container.nodeType === ELEMENT_NODE) {
- clearSuspenseBoundary((container: any), suspenseInstance);
+ } else if (container.nodeType === DOCUMENT_NODE) {
+ clearSuspenseBoundary((container: any).body, suspenseInstance);
+ } else if (container.nodeName === 'HTML') {
+ clearSuspenseBoundary(
+ (container.ownerDocument.body: any),
+ suspenseInstance,
+ );
} else {
- // Document nodes should never contain suspense boundaries.
+ clearSuspenseBoundary((container: any), suspenseInstance);
}
// Retry if any event replaying was blocked on this.
retryIfBlockedOn(container);
@@ -2297,30 +2340,6 @@ export function releaseSingletonInstance(instance: Instance): void {
detachDeletedInstance(instance);
}
-export function clearSingleton(instance: Instance): void {
- const element: Element = (instance: any);
- let node = element.firstChild;
- while (node) {
- const nextNode = node.nextSibling;
- const nodeName = node.nodeName;
- if (
- isMarkedHoistable(node) ||
- nodeName === 'HEAD' ||
- nodeName === 'BODY' ||
- nodeName === 'SCRIPT' ||
- nodeName === 'STYLE' ||
- (nodeName === 'LINK' &&
- ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
- ) {
- // retain these nodes
- } else {
- element.removeChild(node);
- }
- node = nextNode;
- }
- return;
-}
-
// -------------------
// Resources
// -------------------
diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js
index 92571ad69e10c..70a87c777298c 100644
--- a/packages/react-dom/src/__tests__/ReactDOM-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOM-test.js
@@ -566,4 +566,368 @@ describe('ReactDOM', () => {
' in App (at **)',
]);
});
+
+ it('should render root host components into body scope when the container is a Document', async () => {
+ function App({phase}) {
+ return (
+ <>
+ {phase < 1 ? null :
before..
}
+
+
+ {phase < 1 ? null : inside..
}
+
+
+ {phase < 1 ? null : after..
}
+ >
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(document);
+ await act(() => {
+ root.render( cannot be a child of <#document>']);
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before
..inside
inside
..after
after
',
+ );
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before
before..
..inside
inside
inside..
..after
after
after..
',
+ );
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before..
..inside
inside..
..after
after..
',
+ );
+
+ await act(() => {
+ root.unmount();
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '',
+ );
+ });
+
+ it('should render root host components into body scope when the container is a the tag', async () => {
+ function App({phase}) {
+ return (
+ <>
+ {phase < 1 ? null :
..before
}
+ {phase < 3 ?
before
: null}
+ {phase < 2 ? null :
before..
}
+
+ {phase < 1 ? null :
}
+ {phase < 3 ?
: null}
+ {phase < 2 ? null :
}
+
+
+ {phase < 1 ? null :
..inside
}
+ {phase < 3 ?
inside
: null}
+ {phase < 2 ? null :
inside..
}
+
+ {phase < 1 ? null :
..after
}
+ {phase < 3 ?
after
: null}
+ {phase < 2 ? null :
after..
}
+ >
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(document.documentElement);
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
before
inside
after
',
+ );
+
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
+ // root of the application
+ assertConsoleErrorDev(['In HTML,
cannot be a child of ']);
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before
..inside
inside
..after
after
',
+ );
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before
before..
..inside
inside
inside..
..after
after
after..
',
+ );
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before..
..inside
inside..
..after
after..
',
+ );
+
+ await act(() => {
+ root.unmount();
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '',
+ );
+ });
+
+ it('should render root host components into body scope when the container is a the tag', async () => {
+ function App({phase}) {
+ return (
+ <>
+ {phase < 1 ? null :
..before
}
+ {phase < 3 ?
before
: null}
+ {phase < 2 ? null :
before..
}
+
+ {phase < 1 ? null :
}
+ {phase < 3 ?
: null}
+ {phase < 2 ? null :
}
+
+ {phase < 1 ? null :
..inside
}
+ {phase < 3 ?
inside
: null}
+ {phase < 2 ? null :
inside..
}
+ {phase < 1 ? null :
..after
}
+ {phase < 3 ?
after
: null}
+ {phase < 2 ? null :
after..
}
+ >
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(document.body);
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
before
inside
after
',
+ );
+
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
+ // root of the application
+ assertConsoleErrorDev(['In HTML, cannot be a child of ']);
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before
..inside
inside
..after
after
',
+ );
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before
before..
..inside
inside
inside..
..after
after
after..
',
+ );
+
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
..before
before..
..inside
inside..
..after
after..
',
+ );
+
+ await act(() => {
+ root.unmount();
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '',
+ );
+ });
+
+ it('should render children of into the document head even when the container is inside the document body', async () => {
+ function App({phase}) {
+ return (
+ <>
+
before
+
+ {phase < 1 ? null :
}
+ {phase < 3 ?
: null}
+ {phase < 2 ? null :
}
+
+
after
+ >
+ );
+ }
+
+ const container = document.createElement('main');
+ document.body.append(container);
+ const root = ReactDOMClient.createRoot(container);
+ await act(() => {
+ root.render(
);
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ '
before
after
',
+ );
+
+ // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the
+ // root of the application
+ assertConsoleErrorDev(['In HTML, cannot be a child of
']);
+
+ await act(() => {
+ root.render( );
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ 'before
after
',
+ );
+
+ await act(() => {
+ root.render( );
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ 'before
after
',
+ );
+
+ await act(() => {
+ root.render( );
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ 'before
after
',
+ );
+
+ await act(() => {
+ root.unmount();
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ ' ',
+ );
+ });
+
+ it('can render a Suspense boundary above the tag', async () => {
+ let resolveA;
+ const promiseA = new Promise(resolve => {
+ resolveA = resolve;
+ });
+ let resolveB;
+ const promiseB = new Promise(resolve => {
+ resolveB = resolve;
+ });
+ let resolveC;
+ const promiseC = new Promise(resolve => {
+ resolveC = resolve;
+ });
+
+ function ComponentA() {
+ return React.use(promiseA);
+ }
+
+ function ComponentB() {
+ return React.use(promiseB);
+ }
+
+ function ComponentC() {
+ return React.use(promiseC);
+ }
+
+ function App({phase}) {
+ let content;
+ switch (phase) {
+ case 0:
+ content = ;
+ break;
+ case 1:
+ content = ;
+ break;
+ default:
+ content = ;
+ }
+ return (
+
+
+ fallback
+
+
+ }>
+
+
+
+
+
+ {content}
+
+
+
+ );
+ }
+
+ const root = ReactDOMClient.createRoot(document);
+ await act(() => {
+ root.render( );
+ });
+ // The initial render is blocked by promiseA so we see the fallback Document
+ expect(document.documentElement.outerHTML).toBe(
+ 'fallback
',
+ );
+
+ await act(() => {
+ resolveA('hello world');
+ });
+ // When promiseA resolves we see the primary Document
+ expect(document.documentElement.outerHTML).toBe(
+ 'hello world
',
+ );
+
+ await act(() => {
+ root.render( );
+ });
+ // When we switch to rendering ComponentB synchronously we have to put the Document back into fallback
+ // The primary content remains hidden until promiseB resolves
+ expect(document.documentElement.outerHTML).toBe(
+ 'hello world
fallback
',
+ );
+
+ await act(() => {
+ resolveB('hello you!');
+ });
+ // When promiseB resolves we see the new primary content inside the primary Document
+ // style attributes stick around after being unhidden by the Suspense boundary
+ expect(document.documentElement.outerHTML).toBe(
+ 'hello you!
',
+ );
+
+ await act(() => {
+ React.startTransition(() => {
+ root.render( );
+ });
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ 'hello you!
',
+ );
+
+ await act(() => {
+ resolveC('goodbye!');
+ });
+ expect(document.documentElement.outerHTML).toBe(
+ 'goodbye!
',
+ );
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
index 5f50ceb207aba..bdd50c9d4f469 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
@@ -31,7 +31,6 @@ let hasErrored = false;
let fatalError = undefined;
let renderOptions;
let waitForAll;
-let waitForThrow;
let assertLog;
let Scheduler;
let clientAct;
@@ -76,7 +75,6 @@ describe('ReactDOMFloat', () => {
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
- waitForThrow = InternalTestUtils.waitForThrow;
assertLog = InternalTestUtils.assertLog;
clientAct = InternalTestUtils.act;
assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev;
@@ -507,14 +505,7 @@ describe('ReactDOMFloat', () => {
>,
);
- let aggregateError = await waitForThrow();
- expect(aggregateError.errors.length).toBe(2);
- expect(aggregateError.errors[0].message).toContain(
- 'Invalid insertion of NOSCRIPT',
- );
- expect(aggregateError.errors[1].message).toContain(
- 'The node to be removed is not a child of this node',
- );
+ await waitForAll([]);
assertConsoleErrorDev([
[
'Cannot render outside the main document. Try moving it into the root tag.',
@@ -579,14 +570,7 @@ describe('ReactDOMFloat', () => {
>,
);
- aggregateError = await waitForThrow();
- expect(aggregateError.errors.length).toBe(2);
- expect(aggregateError.errors[0].message).toContain(
- 'Invalid insertion of LINK',
- );
- expect(aggregateError.errors[1].message).toContain(
- 'The node to be removed is not a child of this node',
- );
+ await waitForAll([]);
assertConsoleErrorDev([
[
'Cannot render a outside the main document without knowing its precedence. ' +
@@ -644,14 +628,7 @@ describe('ReactDOMFloat', () => {
>,
);
- aggregateError = await waitForThrow();
- expect(aggregateError.errors.length).toBe(2);
- expect(aggregateError.errors[0].message).toContain(
- 'Invalid insertion of LINK',
- );
- expect(aggregateError.errors[1].message).toContain(
- 'The node to be removed is not a child of this node',
- );
+ await waitForAll([]);
assertConsoleErrorDev(
[
'Cannot render a with onLoad or onError listeners outside the main document. ' +
@@ -660,6 +637,7 @@ describe('ReactDOMFloat', () => {
],
{withoutStack: true},
);
+ return;
});
it('can acquire a resource after releasing it in the same commit', async () => {
@@ -1257,6 +1235,13 @@ body {
pipe(writable);
});
+ expect(getMeaningfulChildren(document)).toEqual(
+
+
+ loading...
+ ,
+ );
+
await act(() => {
resolveText('unblock');
});
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index a7bb6e6df5acd..5bb468a52dce4 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -97,6 +97,7 @@ import {
Passive,
DidDefer,
ViewTransitionNamedStatic,
+ MutationStatic,
} from './ReactFiberFlags';
import {
disableLegacyContext,
@@ -1703,21 +1704,13 @@ function updateHostSingleton(
}
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);
- }
+ reconcileChildren(current, workInProgress, nextChildren, renderLanes);
markRef(current, workInProgress);
+ if (current === null) {
+ // We mark Singletons with a static flag to more efficiently manage their
+ // ownership of the singleton host instance when in offscreen trees including Suspense
+ workInProgress.flags |= MutationStatic;
+ }
return workInProgress.child;
}
diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
index 8ecc2d0de0fac..159c12bd4bcf2 100644
--- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
@@ -47,8 +47,9 @@ import {
commitHydratedSuspenseInstance,
removeChildFromContainer,
removeChild,
- clearSingleton,
acquireSingletonInstance,
+ releaseSingletonInstance,
+ isSingletonScope,
} from './ReactFiberConfig';
import {captureCommitPhaseError} from './ReactFiberWorkLoop';
import {trackHostMutation} from './ReactFiberMutationTracking';
@@ -218,7 +219,9 @@ function isHostParent(fiber: Fiber): boolean {
fiber.tag === HostComponent ||
fiber.tag === HostRoot ||
(supportsResources ? fiber.tag === HostHoistable : false) ||
- (supportsSingletons ? fiber.tag === HostSingleton : false) ||
+ (supportsSingletons
+ ? fiber.tag === HostSingleton && isSingletonScope(fiber.type)
+ : false) ||
fiber.tag === HostPortal
);
}
@@ -245,9 +248,19 @@ function getHostSibling(fiber: Fiber): ?Instance {
while (
node.tag !== HostComponent &&
node.tag !== HostText &&
- (!supportsSingletons ? true : node.tag !== HostSingleton) &&
node.tag !== DehydratedFragment
) {
+ // If this is a host singleton we go deeper if it's not a special
+ // singleton scope. If it is a singleton scope we skip over it because
+ // you only insert against this scope when you are already inside of it
+ if (
+ supportsSingletons &&
+ node.tag === HostSingleton &&
+ isSingletonScope(node.type)
+ ) {
+ continue siblings;
+ }
+
// If it is not host node and, we might have a host node inside it.
// Try to search down until we find one.
if (node.flags & Placement) {
@@ -286,23 +299,30 @@ function insertOrAppendPlacementNodeIntoContainer(
appendChildToContainer(parent, stateNode);
}
trackHostMutation();
- } else if (
- tag === HostPortal ||
- (supportsSingletons ? tag === HostSingleton : false)
- ) {
+ return;
+ } else if (tag === HostPortal) {
// 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) {
- insertOrAppendPlacementNodeIntoContainer(child, before, parent);
- let sibling = child.sibling;
- while (sibling !== null) {
- insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
- sibling = sibling.sibling;
- }
+ return;
+ }
+
+ if (
+ (supportsSingletons ? tag === HostSingleton : false) &&
+ isSingletonScope(node.type)
+ ) {
+ // This singleton is the parent of deeper nodes and needs to become
+ // the parent for child insertions and appends
+ parent = node.stateNode;
+ }
+
+ const child = node.child;
+ if (child !== null) {
+ insertOrAppendPlacementNodeIntoContainer(child, before, parent);
+ let sibling = child.sibling;
+ while (sibling !== null) {
+ insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
+ sibling = sibling.sibling;
}
}
}
@@ -322,23 +342,30 @@ function insertOrAppendPlacementNode(
appendChild(parent, stateNode);
}
trackHostMutation();
- } else if (
- tag === HostPortal ||
- (supportsSingletons ? tag === HostSingleton : false)
- ) {
+ return;
+ } else if (tag === HostPortal) {
// 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) {
- insertOrAppendPlacementNode(child, before, parent);
- let sibling = child.sibling;
- while (sibling !== null) {
- insertOrAppendPlacementNode(sibling, before, parent);
- sibling = sibling.sibling;
- }
+ return;
+ }
+
+ if (
+ (supportsSingletons ? tag === HostSingleton : false) &&
+ isSingletonScope(node.type)
+ ) {
+ // This singleton is the parent of deeper nodes and needs to become
+ // the parent for child insertions and appends
+ parent = node.stateNode;
+ }
+
+ const child = node.child;
+ if (child !== null) {
+ insertOrAppendPlacementNode(child, before, parent);
+ let sibling = child.sibling;
+ while (sibling !== null) {
+ insertOrAppendPlacementNode(sibling, before, parent);
+ sibling = sibling.sibling;
}
}
}
@@ -348,14 +375,6 @@ function commitPlacement(finishedWork: Fiber): void {
return;
}
- if (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);
@@ -546,13 +565,12 @@ export function commitHostHydratedSuspense(
}
}
-export function commitHostSingleton(finishedWork: Fiber) {
+export function commitHostSingletonAcquisition(finishedWork: Fiber) {
const singleton = finishedWork.stateNode;
const props = finishedWork.memoizedProps;
try {
- // This was a new mount, we need to clear and set initial properties
- clearSingleton(singleton);
+ // This was a new mount, acquire the DOM instance and set initial properties
if (__DEV__) {
runWithFiberInDEV(
finishedWork,
@@ -574,3 +592,15 @@ export function commitHostSingleton(finishedWork: Fiber) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
+
+export function commitHostSingletonRelease(releasingWork: Fiber) {
+ if (__DEV__) {
+ runWithFiberInDEV(
+ releasingWork,
+ releaseSingletonInstance,
+ releasingWork.stateNode,
+ );
+ } else {
+ releaseSingletonInstance(releasingWork.stateNode);
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index e57f54e6687e6..8807b81502d47 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -152,7 +152,6 @@ import {
prepareForCommit,
beforeActiveInstanceBlur,
detachDeletedInstance,
- releaseSingletonInstance,
getHoistableRoot,
acquireResource,
releaseResource,
@@ -173,6 +172,7 @@ import {
hasInstanceChanged,
hasInstanceAffectedParent,
wasInstanceInViewport,
+ isSingletonScope,
} from './ReactFiberConfig';
import {
captureCommitPhaseError,
@@ -246,7 +246,8 @@ import {
commitHostHydratedSuspense,
commitHostRemoveChildFromContainer,
commitHostRemoveChild,
- commitHostSingleton,
+ commitHostSingletonAcquisition,
+ commitHostSingletonRelease,
} from './ReactFiberCommitHostEffects';
import {
viewTransitionMutationContext,
@@ -1840,8 +1841,7 @@ function hideOrUnhideAllChildren(finishedWork: Fiber, isHidden: boolean) {
while (true) {
if (
node.tag === HostComponent ||
- (supportsResources ? node.tag === HostHoistable : false) ||
- (supportsSingletons ? node.tag === HostSingleton : false)
+ (supportsResources ? node.tag === HostHoistable : false)
) {
if (hostSubtreeRoot === null) {
hostSubtreeRoot = node;
@@ -1994,7 +1994,17 @@ function commitDeletionEffects(
let parent: null | Fiber = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
- case HostSingleton:
+ case HostSingleton: {
+ if (supportsSingletons) {
+ if (isSingletonScope(parent.type)) {
+ hostParent = parent.stateNode;
+ hostParentIsContainer = false;
+ break findParent;
+ }
+ break;
+ }
+ // Expected fallthrough when supportsSingletons is false
+ }
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
@@ -2083,7 +2093,10 @@ function commitDeletionEffectsOnFiber(
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
- hostParent = deletedFiber.stateNode;
+ if (isSingletonScope(deletedFiber.type)) {
+ hostParent = deletedFiber.stateNode;
+ hostParentIsContainer = false;
+ }
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
@@ -2095,7 +2108,7 @@ function commitDeletionEffectsOnFiber(
// 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);
+ commitHostSingletonRelease(deletedFiber);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
@@ -2684,12 +2697,27 @@ function commitMutationEffectsOnFiber(
}
case HostSingleton: {
if (supportsSingletons) {
- if (flags & Update) {
- const previousWork = finishedWork.alternate;
- if (previousWork === null) {
- commitHostSingleton(finishedWork);
+ recursivelyTraverseMutationEffects(root, finishedWork, lanes);
+ commitReconciliationEffects(finishedWork, lanes);
+ if (flags & Ref) {
+ if (!offscreenSubtreeWasHidden && current !== null) {
+ safelyDetachRef(current, current.return);
+ }
+ }
+ const newlyRevealed =
+ offscreenSubtreeWasHidden && !offscreenSubtreeIsHidden;
+ if (newlyRevealed) {
+ commitHostSingletonAcquisition(finishedWork);
+ } else if (flags & Update) {
+ if (current === null) {
+ commitHostSingletonAcquisition(finishedWork);
+ } else {
+ const newProps = finishedWork.memoizedProps;
+ const oldProps = current.memoizedProps;
+ commitHostUpdate(finishedWork, newProps, oldProps);
}
}
+ break;
}
// Fall through
}
@@ -3371,8 +3399,15 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
recursivelyTraverseDisappearLayoutEffects(finishedWork);
break;
}
+ case HostSingleton: {
+ // TODO (Offscreen) Check: flags & RefStatic
+ safelyDetachRef(finishedWork, finishedWork.return);
+ commitHostSingletonRelease(finishedWork);
+
+ recursivelyTraverseDisappearLayoutEffects(finishedWork);
+ break;
+ }
case HostHoistable:
- case HostSingleton:
case HostComponent: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoSingletons.js b/packages/react-reconciler/src/ReactFiberConfigWithNoSingletons.js
index f592e9518591f..2015b21f6e2ee 100644
--- a/packages/react-reconciler/src/ReactFiberConfigWithNoSingletons.js
+++ b/packages/react-reconciler/src/ReactFiberConfigWithNoSingletons.js
@@ -21,7 +21,6 @@ function shim(...args: any): any {
// 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/ReactFiberFlags.js b/packages/react-reconciler/src/ReactFiberFlags.js
index d5dcc3ab7c9c0..6dd24d4e76194 100644
--- a/packages/react-reconciler/src/ReactFiberFlags.js
+++ b/packages/react-reconciler/src/ReactFiberFlags.js
@@ -68,22 +68,23 @@ export const Forked = /* */ 0b0000000000100000000000000000
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const SnapshotStatic = /* */ 0b0000000001000000000000000000000;
-export const LayoutStatic = /* */ 0b0000000010000000000000000000000;
+export const MutationStatic = /* */ 0b0000000010000000000000000000000;
+export const LayoutStatic = /* */ 0b0000000100000000000000000000000;
export const RefStatic = LayoutStatic;
-export const PassiveStatic = /* */ 0b0000000100000000000000000000000;
-export const MaySuspendCommit = /* */ 0b0000001000000000000000000000000;
+export const PassiveStatic = /* */ 0b0000001000000000000000000000000;
+export const MaySuspendCommit = /* */ 0b0000010000000000000000000000000;
// ViewTransitionNamedStatic tracks explicitly name ViewTransition components deeply
// that might need to be visited during clean up. This is similar to SnapshotStatic
// if there was any other use for it.
export const ViewTransitionNamedStatic = /* */ SnapshotStatic;
// ViewTransitionStatic tracks whether there are an ViewTransition components from
// the nearest HostComponent down. It resets at every HostComponent level.
-export const ViewTransitionStatic = /* */ 0b0000010000000000000000000000000;
+export const ViewTransitionStatic = /* */ 0b0000100000000000000000000000000;
// Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
-export const PlacementDEV = /* */ 0b0000100000000000000000000000000;
-export const MountLayoutDev = /* */ 0b0001000000000000000000000000000;
-export const MountPassiveDev = /* */ 0b0010000000000000000000000000000;
+export const PlacementDEV = /* */ 0b0001000000000000000000000000000;
+export const MountLayoutDev = /* */ 0b0010000000000000000000000000000;
+export const MountPassiveDev = /* */ 0b0100000000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
@@ -130,6 +131,8 @@ export const PassiveTransitionMask: number = PassiveMask | Update | Placement;
// This allows certain concepts to persist without recalculating them,
// e.g. whether a subtree contains passive effects or portals.
export const StaticMask =
+ SnapshotStatic |
+ MutationStatic |
LayoutStatic |
PassiveStatic |
RefStatic |
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
index 5d6ab3224ee51..ea1f2d0cec3af 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
@@ -232,7 +232,6 @@ export const suspendResource = $$$config.suspendResource;
// -------------------
export const supportsSingletons = $$$config.supportsSingletons;
export const resolveSingletonInstance = $$$config.resolveSingletonInstance;
-export const clearSingleton = $$$config.clearSingleton;
export const acquireSingletonInstance = $$$config.acquireSingletonInstance;
export const releaseSingletonInstance = $$$config.releaseSingletonInstance;
export const isHostSingletonType = $$$config.isHostSingletonType;