Skip to content

Commit

Permalink
[Fiber] Support Suspense boundaries anywhere (excluding hydration)
Browse files Browse the repository at this point in the history
This is a follow up to #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)
  • Loading branch information
gnoff committed Jan 24, 2025
1 parent b25bcd4 commit c5a9a78
Show file tree
Hide file tree
Showing 8 changed files with 656 additions and 175 deletions.
135 changes: 77 additions & 58 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
// -------------------
Expand Down
Loading

0 comments on commit c5a9a78

Please sign in to comment.