Skip to content

Commit 4ee3c93

Browse files
committed
[Fizz] Support Suspense boundaries anywhere (#32069)
Suspense is meant to be composable but there has been a lonstanding limitation with using Suspense above the `<body>` tag of an HTML document due to peculiarities of how HTML is parsed. For instance if you used Suspense to render an entire HTML document and had a fallback that might flush an alternate Document the comment nodes which describe this boundary scope won't be where they need to be in the DOM for client React to properly hydrate them. This is somewhat a problem of our own making in that we have a concept of a Preamble and we leave the closing body and html tags behind until streaming has completed which produces a valid HTML document that also matches the DOM structure that would be parsed from it. However Preambles as a concept are too important to features like Float to imagine moving away from this model and so we can either choose to just accept that you cannot use Suspense anywhere except inside the `<body>` or we can build special support for Suspense into react-dom that has a coherent semantic with how HTML documents are written and parsed. This change implements Suspense support for react-dom/server by correctly serializing boundaries during rendering, prerendering, and resumgin on the server. It does not yet support Suspense everywhere on the client but this will arrive in a subsequent change. In practice Suspense cannot be used above the `<body>` tag today so this is not a breaking change since no programs in the wild could be using this feature anyway. React's streaming rendering of HTML doesn't lend itself to replacing the contents of the documentElement, head, or body of a Document. These are already special cased in fiber as HostSingletons and similarly for Fizz the values we render for these tags must never be updated by the Fizz runtime once written. To accomplish these we redefine the Preamble as the tags that represent these three singletons plus the contents of the document.head. If you use Suspense above any part of the Preamble then nothing will be written to the destination until the boundary is no longer pending. If the boundary completes then the preamble from within that boudnary will be output. If the boundary postpones or errors then the preamble from the fallback will be used instead. Additionally, by default anything that is not part of the preamble is implicitly in body scope. This leads to the somewhat counterintuitive consequence that the comment nodes we use to mark the borders of a Suspense boundary in Fizz can appear INSIDE the preamble that was rendered within it. ```typescript render(( <Suspense> <html lang="en"> <body> <div>hello world</div> </body> </html> </Suspense> )) ``` will produce an HTML document like this ```html <!DOCTYPE html> <html lang="en"> <head></head> <body> <!--$--> <-- this is the comment Node representing the outermost Suspense <div>hello world</div> <$--/$--> </body> </html> ``` Later when I update Fiber to support Suspense anywhere hydration will similarly start implicitly in the document body when the root is part of the preamble (the document or one of it's singletons). DiffTrain build for [b25bcd4](b25bcd4)
1 parent ceb3aaf commit 4ee3c93

36 files changed

+2200
-777
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
61e713c1d31976175316c8256f4be14ba8bbdb29
1+
b25bcd460f98a0b89e5a7199a6c88112163d961f
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
61e713c1d31976175316c8256f4be14ba8bbdb29
1+
b25bcd460f98a0b89e5a7199a6c88112163d961f

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1956,7 +1956,7 @@ __DEV__ &&
19561956
exports.useTransition = function () {
19571957
return resolveDispatcher().useTransition();
19581958
};
1959-
exports.version = "19.1.0-www-classic-61e713c1-20250116";
1959+
exports.version = "19.1.0-www-classic-b25bcd46-20250117";
19601960
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
19611961
"function" ===
19621962
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1956,7 +1956,7 @@ __DEV__ &&
19561956
exports.useTransition = function () {
19571957
return resolveDispatcher().useTransition();
19581958
};
1959-
exports.version = "19.1.0-www-modern-61e713c1-20250116";
1959+
exports.version = "19.1.0-www-modern-b25bcd46-20250117";
19601960
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
19611961
"function" ===
19621962
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,4 @@ exports.useSyncExternalStore = function (
630630
exports.useTransition = function () {
631631
return ReactSharedInternals.H.useTransition();
632632
};
633-
exports.version = "19.1.0-www-classic-61e713c1-20250116";
633+
exports.version = "19.1.0-www-classic-b25bcd46-20250117";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -630,4 +630,4 @@ exports.useSyncExternalStore = function (
630630
exports.useTransition = function () {
631631
return ReactSharedInternals.H.useTransition();
632632
};
633-
exports.version = "19.1.0-www-modern-61e713c1-20250116";
633+
exports.version = "19.1.0-www-modern-b25bcd46-20250117";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ exports.useSyncExternalStore = function (
634634
exports.useTransition = function () {
635635
return ReactSharedInternals.H.useTransition();
636636
};
637-
exports.version = "19.1.0-www-classic-61e713c1-20250116";
637+
exports.version = "19.1.0-www-classic-b25bcd46-20250117";
638638
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
639639
"function" ===
640640
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ exports.useSyncExternalStore = function (
634634
exports.useTransition = function () {
635635
return ReactSharedInternals.H.useTransition();
636636
};
637-
exports.version = "19.1.0-www-modern-61e713c1-20250116";
637+
exports.version = "19.1.0-www-modern-b25bcd46-20250117";
638638
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
639639
"function" ===
640640
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16950,10 +16950,10 @@ __DEV__ &&
1695016950
(function () {
1695116951
var internals = {
1695216952
bundleType: 1,
16953-
version: "19.1.0-www-classic-61e713c1-20250116",
16953+
version: "19.1.0-www-classic-b25bcd46-20250117",
1695416954
rendererPackageName: "react-art",
1695516955
currentDispatcherRef: ReactSharedInternals,
16956-
reconcilerVersion: "19.1.0-www-classic-61e713c1-20250116"
16956+
reconcilerVersion: "19.1.0-www-classic-b25bcd46-20250117"
1695716957
};
1695816958
internals.overrideHookState = overrideHookState;
1695916959
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -16987,7 +16987,7 @@ __DEV__ &&
1698716987
exports.Shape = Shape;
1698816988
exports.Surface = Surface;
1698916989
exports.Text = Text;
16990-
exports.version = "19.1.0-www-classic-61e713c1-20250116";
16990+
exports.version = "19.1.0-www-classic-b25bcd46-20250117";
1699116991
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1699216992
"function" ===
1699316993
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.modern.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16722,10 +16722,10 @@ __DEV__ &&
1672216722
(function () {
1672316723
var internals = {
1672416724
bundleType: 1,
16725-
version: "19.1.0-www-modern-61e713c1-20250116",
16725+
version: "19.1.0-www-modern-b25bcd46-20250117",
1672616726
rendererPackageName: "react-art",
1672716727
currentDispatcherRef: ReactSharedInternals,
16728-
reconcilerVersion: "19.1.0-www-modern-61e713c1-20250116"
16728+
reconcilerVersion: "19.1.0-www-modern-b25bcd46-20250117"
1672916729
};
1673016730
internals.overrideHookState = overrideHookState;
1673116731
internals.overrideHookStateDeletePath = overrideHookStateDeletePath;
@@ -16759,7 +16759,7 @@ __DEV__ &&
1675916759
exports.Shape = Shape;
1676016760
exports.Surface = Surface;
1676116761
exports.Text = Text;
16762-
exports.version = "19.1.0-www-modern-61e713c1-20250116";
16762+
exports.version = "19.1.0-www-modern-b25bcd46-20250117";
1676316763
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
1676416764
"function" ===
1676516765
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

0 commit comments

Comments
 (0)