mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Fiber] Support Suspense boundaries anywhere (excluding hydration) (#32163)
This is a follow up to https://github.com/facebook/react/pull/32069 In the prior change I updated Fizz to allow you to render Suspense boundaries at any level within a react-dom application by treating the document body as the default render scope. This change updates Fiber to provide similar semantics. Note that this update still does not deliver hydration so unifying the Fizz and Fiber implementations in a single App is not possible yet. The implementation required a rework of the getHostSibling and getHostParent algorithms. Now most HostSingletons are invisible from a host positioning perspective. Head is special in that it is a valid host scope so when you have Placements inside of it, it will act as the parent. But body, and html, will not directly participate in host positioning. Additionally to support flipping to a fallback html, head, and body tag in a Suspense fallback I updated the offscreen hiding/unhide logic to pierce through singletons when lookin for matching hidable nod boundaries anywhere (excluding hydration)
This commit is contained in:
parent
37906d4dfb
commit
c492f97541
|
|
@ -799,24 +799,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
|
||||
|
|
@ -853,21 +866,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 {
|
||||
|
|
@ -913,11 +940,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(
|
||||
|
|
@ -965,10 +1003,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);
|
||||
|
|
@ -2299,30 +2342,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
|
||||
// -------------------
|
||||
|
|
|
|||
408
packages/react-dom/src/__tests__/ReactDOM-test.js
vendored
408
packages/react-dom/src/__tests__/ReactDOM-test.js
vendored
|
|
@ -566,4 +566,412 @@ 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 : <div>..before</div>}
|
||||
{phase < 3 ? <div>before</div> : null}
|
||||
{phase < 2 ? null : <div>before..</div>}
|
||||
<html lang="en">
|
||||
<head data-h="">
|
||||
{phase < 1 ? null : <meta itemProp="" content="..head" />}
|
||||
{phase < 3 ? <meta itemProp="" content="head" /> : null}
|
||||
{phase < 2 ? null : <meta itemProp="" content="head.." />}
|
||||
</head>
|
||||
<body data-b="">
|
||||
{phase < 1 ? null : <div>..inside</div>}
|
||||
{phase < 3 ? <div>inside</div> : null}
|
||||
{phase < 2 ? null : <div>inside..</div>}
|
||||
</body>
|
||||
</html>
|
||||
{phase < 1 ? null : <div>..after</div>}
|
||||
{phase < 3 ? <div>after</div> : null}
|
||||
{phase < 2 ? null : <div>after..</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(document);
|
||||
await act(() => {
|
||||
root.render(<App phase={0} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head data-h=""><meta itemprop="" content="head"></head><body data-b=""><div>before</div><div>inside</div><div>after</div></body></html>',
|
||||
);
|
||||
|
||||
// @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, <div> cannot be a child of <#document>']);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={1} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body data-b=""><div>..before</div><div>before</div><div>..inside</div><div>inside</div><div>..after</div><div>after</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={2} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before</div><div>before..</div><div>..inside</div><div>inside</div><div>inside..</div><div>..after</div><div>after</div><div>after..</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={3} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before..</div><div>..inside</div><div>inside..</div><div>..after</div><div>after..</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head></head><body></body></html>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render root host components into body scope when the container is a the <html> tag', async () => {
|
||||
function App({phase}) {
|
||||
return (
|
||||
<>
|
||||
{phase < 1 ? null : <div>..before</div>}
|
||||
{phase < 3 ? <div>before</div> : null}
|
||||
{phase < 2 ? null : <div>before..</div>}
|
||||
<head data-h="">
|
||||
{phase < 1 ? null : <meta itemProp="" content="..head" />}
|
||||
{phase < 3 ? <meta itemProp="" content="head" /> : null}
|
||||
{phase < 2 ? null : <meta itemProp="" content="head.." />}
|
||||
</head>
|
||||
<body data-b="">
|
||||
{phase < 1 ? null : <div>..inside</div>}
|
||||
{phase < 3 ? <div>inside</div> : null}
|
||||
{phase < 2 ? null : <div>inside..</div>}
|
||||
</body>
|
||||
{phase < 1 ? null : <div>..after</div>}
|
||||
{phase < 3 ? <div>after</div> : null}
|
||||
{phase < 2 ? null : <div>after..</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(document.documentElement);
|
||||
await act(() => {
|
||||
root.render(<App phase={0} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="head"></head><body data-b=""><div>before</div><div>inside</div><div>after</div></body></html>',
|
||||
);
|
||||
|
||||
// @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, <div> cannot be a child of <html>']);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={1} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body data-b=""><div>..before</div><div>before</div><div>..inside</div><div>inside</div><div>..after</div><div>after</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={2} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before</div><div>before..</div><div>..inside</div><div>inside</div><div>inside..</div><div>..after</div><div>after</div><div>after..</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={3} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body data-b=""><div>..before</div><div>before..</div><div>..inside</div><div>inside..</div><div>..after</div><div>after..</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head></head><body></body></html>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render root host components into body scope when the container is a the <body> tag', async () => {
|
||||
function App({phase}) {
|
||||
return (
|
||||
<>
|
||||
{phase < 1 ? null : <div>..before</div>}
|
||||
{phase < 3 ? <div>before</div> : null}
|
||||
{phase < 2 ? null : <div>before..</div>}
|
||||
<head data-h="">
|
||||
{phase < 1 ? null : <meta itemProp="" content="..head" />}
|
||||
{phase < 3 ? <meta itemProp="" content="head" /> : null}
|
||||
{phase < 2 ? null : <meta itemProp="" content="head.." />}
|
||||
</head>
|
||||
{phase < 1 ? null : <div>..inside</div>}
|
||||
{phase < 3 ? <div>inside</div> : null}
|
||||
{phase < 2 ? null : <div>inside..</div>}
|
||||
{phase < 1 ? null : <div>..after</div>}
|
||||
{phase < 3 ? <div>after</div> : null}
|
||||
{phase < 2 ? null : <div>after..</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(document.body);
|
||||
await act(() => {
|
||||
root.render(<App phase={0} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="head"></head><body><div>before</div><div>inside</div><div>after</div></body></html>',
|
||||
);
|
||||
|
||||
// @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, <head> cannot be a child of <body>']);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={1} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body><div>..before</div><div>before</div><div>..inside</div><div>inside</div><div>..after</div><div>after</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={2} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body><div>..before</div><div>before</div><div>before..</div><div>..inside</div><div>inside</div><div>inside..</div><div>..after</div><div>after</div><div>after..</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={3} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body><div>..before</div><div>before..</div><div>..inside</div><div>inside..</div><div>..after</div><div>after..</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head></head><body></body></html>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render children of <head> into the document head even when the container is inside the document body', async () => {
|
||||
function App({phase}) {
|
||||
return (
|
||||
<>
|
||||
<div>before</div>
|
||||
<head data-h="">
|
||||
{phase < 1 ? null : <meta itemProp="" content="..head" />}
|
||||
{phase < 3 ? <meta itemProp="" content="head" /> : null}
|
||||
{phase < 2 ? null : <meta itemProp="" content="head.." />}
|
||||
</head>
|
||||
<div>after</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.createElement('main');
|
||||
document.body.append(container);
|
||||
const root = ReactDOMClient.createRoot(container);
|
||||
await act(() => {
|
||||
root.render(<App phase={0} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="head"></head><body><main><div>before</div><div>after</div></main></body></html>',
|
||||
);
|
||||
|
||||
// @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, <head> cannot be a child of <main>']);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={1} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"></head><body><main><div>before</div><div>after</div></main></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={2} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head"><meta itemprop="" content="head.."></head><body><main><div>before</div><div>after</div></main></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.render(<App phase={3} />);
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head data-h=""><meta itemprop="" content="..head"><meta itemprop="" content="head.."></head><body><main><div>before</div><div>after</div></main></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html><head></head><body><main></main></body></html>',
|
||||
);
|
||||
});
|
||||
|
||||
it('can render a Suspense boundary above the <html> tag', async () => {
|
||||
let suspendOnNewPromise;
|
||||
let resolveCurrentPromise;
|
||||
let currentPromise;
|
||||
function createNewPromise() {
|
||||
currentPromise = new Promise(r => {
|
||||
resolveCurrentPromise = r;
|
||||
});
|
||||
return currentPromise;
|
||||
}
|
||||
createNewPromise();
|
||||
function Comp() {
|
||||
const [promise, setPromise] = React.useState(currentPromise);
|
||||
suspendOnNewPromise = () => {
|
||||
setPromise(createNewPromise());
|
||||
};
|
||||
React.use(promise);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallback = (
|
||||
<html data-fallback="">
|
||||
<body data-fallback="">
|
||||
<div>fallback</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
const main = (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta itemProp="" content="primary" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<Message />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
let suspendOnNewMessage;
|
||||
let currentMessage;
|
||||
let resolveCurrentMessage;
|
||||
function createNewMessage() {
|
||||
currentMessage = new Promise(r => {
|
||||
resolveCurrentMessage = r;
|
||||
});
|
||||
return currentMessage;
|
||||
}
|
||||
createNewMessage();
|
||||
resolveCurrentMessage('hello world');
|
||||
function Message() {
|
||||
const [pendingMessage, setPendingMessage] =
|
||||
React.useState(currentMessage);
|
||||
suspendOnNewMessage = () => {
|
||||
setPendingMessage(createNewMessage());
|
||||
};
|
||||
return React.use(pendingMessage);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<React.Suspense fallback={fallback}>
|
||||
<Comp />
|
||||
{main}
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactDOMClient.createRoot(document);
|
||||
await act(() => {
|
||||
root.render(<App />);
|
||||
});
|
||||
// The initial render is blocked by promiseA so we see the fallback Document
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html data-fallback=""><head></head><body data-fallback=""><div>fallback</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveCurrentPromise();
|
||||
});
|
||||
// When promiseA resolves we see the primary Document
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary"></head><body><div>hello world</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
suspendOnNewPromise();
|
||||
});
|
||||
// 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(
|
||||
'<html data-fallback=""><head><meta itemprop="" content="primary" style="display: none;"></head><body data-fallback=""><div style="display: none;">hello world</div><div>fallback</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveCurrentPromise();
|
||||
});
|
||||
// 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(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello world</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
React.startTransition(() => {
|
||||
suspendOnNewPromise();
|
||||
});
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello world</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveCurrentPromise();
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello world</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
suspendOnNewMessage();
|
||||
});
|
||||
// When we update the message itself we will be causing updates on the primary content of the Suspense boundary.
|
||||
// The reason we also test for this is to make sure we don't double acquire the document singletons while
|
||||
// disappearing and reappearing layout effects
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html data-fallback=""><head><meta itemprop="" content="primary" style="display: none;"></head><body data-fallback=""><div style="display: none;">hello world</div><div>fallback</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveCurrentMessage('hello you!');
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello you!</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
React.startTransition(() => {
|
||||
suspendOnNewMessage();
|
||||
});
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">hello you!</div></body></html>',
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveCurrentMessage('goodbye!');
|
||||
});
|
||||
expect(document.documentElement.outerHTML).toBe(
|
||||
'<html lang="en"><head><meta itemprop="" content="primary" style=""></head><body><div style="">goodbye!</div></body></html>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
|||
</html>
|
||||
</>,
|
||||
);
|
||||
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 <noscript> outside the main document. Try moving it into the root <head> tag.',
|
||||
|
|
@ -579,14 +570,7 @@ describe('ReactDOMFloat', () => {
|
|||
<link rel="stylesheet" href="foo" />
|
||||
</>,
|
||||
);
|
||||
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 <link rel="stylesheet" /> outside the main document without knowing its precedence. ' +
|
||||
|
|
@ -644,14 +628,7 @@ describe('ReactDOMFloat', () => {
|
|||
</html>
|
||||
</>,
|
||||
);
|
||||
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 <link> 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(
|
||||
<html>
|
||||
<head />
|
||||
<body>loading...</body>
|
||||
</html>,
|
||||
);
|
||||
|
||||
await act(() => {
|
||||
resolveText('unblock');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ import {
|
|||
Passive,
|
||||
DidDefer,
|
||||
ViewTransitionNamedStatic,
|
||||
LayoutStatic,
|
||||
} 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 |= LayoutStatic;
|
||||
}
|
||||
return workInProgress.child;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -1359,22 +1360,24 @@ function commitLayoutEffectOnFiber(
|
|||
}
|
||||
break;
|
||||
}
|
||||
case HostHoistable: {
|
||||
if (supportsResources) {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
finishedWork,
|
||||
committedLanes,
|
||||
);
|
||||
|
||||
if (flags & Ref) {
|
||||
safelyAttachRef(finishedWork, finishedWork.return);
|
||||
case HostSingleton: {
|
||||
if (supportsSingletons) {
|
||||
// We acquire the singleton instance first so it has appropriate
|
||||
// styles before other layout effects run. This isn't perfect because
|
||||
// an early sibling of the singleton may have an effect that can
|
||||
// observe the singleton before it is acquired.
|
||||
// @TODO move this to the mutation phase. The reason it isn't there yet
|
||||
// is it seemingly requires an extra traversal because we need to move the
|
||||
// disappear effect into a phase before the appear phase
|
||||
if (current === null && flags & Update) {
|
||||
// Unlike in the reappear path we only acquire on new mount
|
||||
commitHostSingletonAcquisition(finishedWork);
|
||||
}
|
||||
break;
|
||||
// We fall through to the HostComponent case below.
|
||||
}
|
||||
// Fall through
|
||||
// Fallthrough
|
||||
}
|
||||
case HostSingleton:
|
||||
case HostHoistable:
|
||||
case HostComponent: {
|
||||
recursivelyTraverseLayoutEffects(
|
||||
finishedRoot,
|
||||
|
|
@ -1840,8 +1843,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 +1996,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 +2095,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 +2110,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 +2699,19 @@ 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);
|
||||
}
|
||||
}
|
||||
if (current !== null && flags & Update) {
|
||||
const newProps = finishedWork.memoizedProps;
|
||||
const oldProps = current.memoizedProps;
|
||||
commitHostUpdate(finishedWork, newProps, oldProps);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Fall through
|
||||
}
|
||||
|
|
@ -2960,15 +2982,18 @@ function commitMutationEffectsOnFiber(
|
|||
offscreenInstance._visibility |= OffscreenVisible;
|
||||
}
|
||||
|
||||
const isUpdate = current !== null;
|
||||
if (isHidden) {
|
||||
const isUpdate = current !== null;
|
||||
const wasHiddenByAncestorOffscreen =
|
||||
offscreenSubtreeIsHidden || offscreenSubtreeWasHidden;
|
||||
// Only trigger disapper layout effects if:
|
||||
// Only trigger disappear layout effects if:
|
||||
// - This is an update, not first mount.
|
||||
// - This Offscreen was not hidden before.
|
||||
// - Ancestor Offscreen was not hidden in previous commit.
|
||||
if (isUpdate && !wasHidden && !wasHiddenByAncestorOffscreen) {
|
||||
// - Ancestor Offscreen was not hidden in previous commit or in this commit
|
||||
if (
|
||||
isUpdate &&
|
||||
!wasHidden &&
|
||||
!offscreenSubtreeIsHidden &&
|
||||
!offscreenSubtreeWasHidden
|
||||
) {
|
||||
if (
|
||||
disableLegacyMode ||
|
||||
(finishedWork.mode & ConcurrentMode) !== NoMode
|
||||
|
|
@ -3371,8 +3396,14 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
|
|||
recursivelyTraverseDisappearLayoutEffects(finishedWork);
|
||||
break;
|
||||
}
|
||||
case HostSingleton: {
|
||||
if (supportsSingletons) {
|
||||
// TODO (Offscreen) Check: flags & RefStatic
|
||||
commitHostSingletonRelease(finishedWork);
|
||||
}
|
||||
// Expected fallthrough to HostComponent
|
||||
}
|
||||
case HostHoistable:
|
||||
case HostSingleton:
|
||||
case HostComponent: {
|
||||
// TODO (Offscreen) Check: flags & RefStatic
|
||||
safelyDetachRef(finishedWork, finishedWork.return);
|
||||
|
|
@ -3428,7 +3459,7 @@ export function disappearLayoutEffects(finishedWork: Fiber) {
|
|||
}
|
||||
|
||||
function recursivelyTraverseDisappearLayoutEffects(parentFiber: Fiber) {
|
||||
// TODO (Offscreen) Check: flags & (RefStatic | LayoutStatic)
|
||||
// TODO (Offscreen) Check: subtreeflags & (RefStatic | LayoutStatic)
|
||||
let child = parentFiber.child;
|
||||
while (child !== null) {
|
||||
disappearLayoutEffects(child);
|
||||
|
|
@ -3488,8 +3519,21 @@ export function reappearLayoutEffects(
|
|||
// case HostRoot: {
|
||||
// ...
|
||||
// }
|
||||
case HostSingleton: {
|
||||
if (supportsSingletons) {
|
||||
// We acquire the singleton instance first so it has appropriate
|
||||
// styles before other layout effects run. This isn't perfect because
|
||||
// an early sibling of the singleton may have an effect that can
|
||||
// observe the singleton before it is acquired.
|
||||
// @TODO move this to the mutation phase. The reason it isn't there yet
|
||||
// is it seemingly requires an extra traversal because we need to move the
|
||||
// disappear effect into a phase before the appear phase
|
||||
commitHostSingletonAcquisition(finishedWork);
|
||||
// We fall through to the HostComponent case below.
|
||||
}
|
||||
// Fallthrough
|
||||
}
|
||||
case HostHoistable:
|
||||
case HostSingleton:
|
||||
case HostComponent: {
|
||||
recursivelyTraverseReappearLayoutEffects(
|
||||
finishedRoot,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ 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;
|
||||
export const isSingletonScope = shim;
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ 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;
|
||||
export const isSingletonScope = $$$config.isSingletonScope;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user