Skip to content

Commit 6cd02da

Browse files
committed
[Fiber] Support Suspense boundaries anywhere (excluding hydration)
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 b25bcd4 commit 6cd02da

File tree

8 files changed

+655
-175
lines changed

8 files changed

+655
-175
lines changed

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

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

872899
function createEvent(type: DOMEventName, bubbles: boolean): Event {
@@ -912,11 +939,22 @@ export function removeChildFromContainer(
912939
container: Container,
913940
child: Instance | TextInstance | SuspenseInstance,
914941
): void {
915-
if (container.nodeType === COMMENT_NODE) {
916-
(container.parentNode: any).removeChild(child);
917-
} else {
918-
container.removeChild(child);
942+
let parentNode: Document | Element;
943+
switch (container.nodeType) {
944+
case COMMENT_NODE:
945+
parentNode = (container.parentNode: any);
946+
break;
947+
case DOCUMENT_NODE:
948+
parentNode = (container: any).body;
949+
break;
950+
default:
951+
if (container.nodeName === 'HTML') {
952+
parentNode = (container.ownerDocument.body: any);
953+
} else {
954+
parentNode = (container: any);
955+
}
919956
}
957+
parentNode.removeChild(child);
920958
}
921959

922960
export function clearSuspenseBoundary(
@@ -964,10 +1002,15 @@ export function clearSuspenseBoundaryFromContainer(
9641002
): void {
9651003
if (container.nodeType === COMMENT_NODE) {
9661004
clearSuspenseBoundary((container.parentNode: any), suspenseInstance);
967-
} else if (container.nodeType === ELEMENT_NODE) {
968-
clearSuspenseBoundary((container: any), suspenseInstance);
1005+
} else if (container.nodeType === DOCUMENT_NODE) {
1006+
clearSuspenseBoundary((container: any).body, suspenseInstance);
1007+
} else if (container.nodeName === 'HTML') {
1008+
clearSuspenseBoundary(
1009+
(container.ownerDocument.body: any),
1010+
suspenseInstance,
1011+
);
9691012
} else {
970-
// Document nodes should never contain suspense boundaries.
1013+
clearSuspenseBoundary((container: any), suspenseInstance);
9711014
}
9721015
// Retry if any event replaying was blocked on this.
9731016
retryIfBlockedOn(container);
@@ -2297,30 +2340,6 @@ export function releaseSingletonInstance(instance: Instance): void {
22972340
detachDeletedInstance(instance);
22982341
}
22992342

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

0 commit comments

Comments
 (0)