Skip to content

Commit c492f97

Browse files
authored
[Fiber] Support Suspense boundaries anywhere (excluding hydration) (#32163)
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)
1 parent 37906d4 commit c492f97

File tree

8 files changed

+654
-175
lines changed

8 files changed

+654
-175
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+77-58
Original file line numberDiff line numberDiff line change
@@ -799,24 +799,37 @@ export function appendChildToContainer(
799799
container: Container,
800800
child: Instance | TextInstance,
801801
): void {
802-
let parentNode;
803-
if (container.nodeType === COMMENT_NODE) {
804-
parentNode = (container.parentNode: any);
805-
if (supportsMoveBefore) {
806-
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
807-
parentNode.moveBefore(child, container);
808-
} else {
809-
parentNode.insertBefore(child, container);
802+
let parentNode: Document | Element;
803+
switch (container.nodeType) {
804+
case COMMENT_NODE: {
805+
parentNode = (container.parentNode: any);
806+
if (supportsMoveBefore) {
807+
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
808+
parentNode.moveBefore(child, container);
809+
} else {
810+
parentNode.insertBefore(child, container);
811+
}
812+
return;
810813
}
811-
} else {
812-
parentNode = container;
813-
if (supportsMoveBefore) {
814-
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
815-
parentNode.moveBefore(child, null);
816-
} else {
817-
parentNode.appendChild(child);
814+
case DOCUMENT_NODE: {
815+
parentNode = (container: any).body;
816+
break;
818817
}
818+
default: {
819+
if (container.nodeName === 'HTML') {
820+
parentNode = (container.ownerDocument.body: any);
821+
} else {
822+
parentNode = (container: any);
823+
}
824+
}
825+
}
826+
if (supportsMoveBefore) {
827+
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
828+
parentNode.moveBefore(child, null);
829+
} else {
830+
parentNode.appendChild(child);
819831
}
832+
820833
// This container might be used for a portal.
821834
// If something inside a portal is clicked, that click should bubble
822835
// through the React tree. However, on Mobile Safari the click would
@@ -853,21 +866,35 @@ export function insertInContainerBefore(
853866
child: Instance | TextInstance,
854867
beforeChild: Instance | TextInstance | SuspenseInstance,
855868
): void {
856-
if (container.nodeType === COMMENT_NODE) {
857-
if (supportsMoveBefore) {
858-
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
859-
(container.parentNode: any).moveBefore(child, beforeChild);
860-
} else {
861-
(container.parentNode: any).insertBefore(child, beforeChild);
869+
let parentNode: Document | Element;
870+
switch (container.nodeType) {
871+
case COMMENT_NODE: {
872+
parentNode = (container.parentNode: any);
873+
break;
862874
}
863-
} else {
864-
if (supportsMoveBefore) {
865-
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
866-
container.moveBefore(child, beforeChild);
867-
} else {
868-
container.insertBefore(child, beforeChild);
875+
case DOCUMENT_NODE: {
876+
const ownerDocument: Document = (container: any);
877+
parentNode = (ownerDocument.body: any);
878+
break;
879+
}
880+
default: {
881+
if (container.nodeName === 'HTML') {
882+
parentNode = (container.ownerDocument.body: any);
883+
} else {
884+
parentNode = (container: any);
885+
}
869886
}
870887
}
888+
if (supportsMoveBefore) {
889+
// $FlowFixMe[prop-missing]: We've checked this with supportsMoveBefore.
890+
parentNode.moveBefore(child, beforeChild);
891+
} else {
892+
parentNode.insertBefore(child, beforeChild);
893+
}
894+
}
895+
896+
export function isSingletonScope(type: string): boolean {
897+
return type === 'head';
871898
}
872899

873900
function createEvent(type: DOMEventName, bubbles: boolean): Event {
@@ -913,11 +940,22 @@ export function removeChildFromContainer(
913940
container: Container,
914941
child: Instance | TextInstance | SuspenseInstance,
915942
): void {
916-
if (container.nodeType === COMMENT_NODE) {
917-
(container.parentNode: any).removeChild(child);
918-
} else {
919-
container.removeChild(child);
943+
let parentNode: Document | Element;
944+
switch (container.nodeType) {
945+
case COMMENT_NODE:
946+
parentNode = (container.parentNode: any);
947+
break;
948+
case DOCUMENT_NODE:
949+
parentNode = (container: any).body;
950+
break;
951+
default:
952+
if (container.nodeName === 'HTML') {
953+
parentNode = (container.ownerDocument.body: any);
954+
} else {
955+
parentNode = (container: any);
956+
}
920957
}
958+
parentNode.removeChild(child);
921959
}
922960

923961
export function clearSuspenseBoundary(
@@ -965,10 +1003,15 @@ export function clearSuspenseBoundaryFromContainer(
9651003
): void {
9661004
if (container.nodeType === COMMENT_NODE) {
9671005
clearSuspenseBoundary((container.parentNode: any), suspenseInstance);
968-
} else if (container.nodeType === ELEMENT_NODE) {
969-
clearSuspenseBoundary((container: any), suspenseInstance);
1006+
} else if (container.nodeType === DOCUMENT_NODE) {
1007+
clearSuspenseBoundary((container: any).body, suspenseInstance);
1008+
} else if (container.nodeName === 'HTML') {
1009+
clearSuspenseBoundary(
1010+
(container.ownerDocument.body: any),
1011+
suspenseInstance,
1012+
);
9701013
} else {
971-
// Document nodes should never contain suspense boundaries.
1014+
clearSuspenseBoundary((container: any), suspenseInstance);
9721015
}
9731016
// Retry if any event replaying was blocked on this.
9741017
retryIfBlockedOn(container);
@@ -2299,30 +2342,6 @@ export function releaseSingletonInstance(instance: Instance): void {
22992342
detachDeletedInstance(instance);
23002343
}
23012344

2302-
export function clearSingleton(instance: Instance): void {
2303-
const element: Element = (instance: any);
2304-
let node = element.firstChild;
2305-
while (node) {
2306-
const nextNode = node.nextSibling;
2307-
const nodeName = node.nodeName;
2308-
if (
2309-
isMarkedHoistable(node) ||
2310-
nodeName === 'HEAD' ||
2311-
nodeName === 'BODY' ||
2312-
nodeName === 'SCRIPT' ||
2313-
nodeName === 'STYLE' ||
2314-
(nodeName === 'LINK' &&
2315-
((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
2316-
) {
2317-
// retain these nodes
2318-
} else {
2319-
element.removeChild(node);
2320-
}
2321-
node = nextNode;
2322-
}
2323-
return;
2324-
}
2325-
23262345
// -------------------
23272346
// Resources
23282347
// -------------------

0 commit comments

Comments
 (0)