Skip to content

Commit 691606c

Browse files
committed
Client render dehydrated Suspense boundaries on document load (facebook#31620)
When streaming SSR while hydrating React will wait for Suspense boundaries to be revealed by the SSR stream before attempting to hydrate them. The rationale here is that the Server render is likely further ahead of whatever the client would produce so waiting to let the server stream in the UI is preferable to retrying on the client and possibly delaying how quickly the primary content becomes available. However If the connection closes early (user hits stop for instance) or there is a server error which prevents additional HTML from being delivered to the client this can put React into a broken state where the boundary never resolves nor errors and the hydration never retries that boundary freezing it in it's fallback state. Once the document has fully loaded we know there is not way any additional Suspense boundaries can arrive. This update changes react-dom on the client to schedule client renders for any unfinished Suspense boundaries upon document loading. The technique for client rendering a fallback is pretty straight forward. When hydrating a Suspense boundary if the Document is in 'complete' readyState we interpret pending boundaries as fallback boundaries. If the readyState is not 'complete' we register an event to retry the boundary when the DOMContentLoaded event fires. To test this I needed JSDOM to model readyState. We previously had a temporary implementation of readyState for SSR streaming but I ended up implementing this as a mock of JSDOM that implements a fake readyState that is mutable. It starts off in 'loading' readyState and you can advance it by mutating document.readyState. You can also reset it to 'loading'. It fires events when changing states. This seems like the least invasive way to get closer-to-real-browser behavior in a way that won't require remembering this subtle detail every time you create a test that asserts Suspense resolution order. DiffTrain build for [16d2bbb](facebook@16d2bbb)
1 parent 9d91828 commit 691606c

23 files changed

+654
-534
lines changed

compiled-rn/VERSION_NATIVE_FB

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
19.0.0-native-fb-2a9f4c04-20241122
1+
19.0.0-native-fb-16d2bbbd-20241203

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-dev.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<ddd246bb691d4a72856ff58c8e84e9ab>>
10+
* @generated SignedSource<<cbb4fb73c9e1ccc01854e808c548f830>>
1111
*/
1212

1313
"use strict";
@@ -420,5 +420,5 @@ __DEV__ &&
420420
exports.useFormStatus = function () {
421421
return resolveDispatcher().useHostTransitionStatus();
422422
};
423-
exports.version = "19.0.0-native-fb-2a9f4c04-20241122";
423+
exports.version = "19.0.0-native-fb-16d2bbbd-20241203";
424424
})();

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-prod.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<6333fffa5ab523cc9687eb23bac28b0b>>
10+
* @generated SignedSource<<239a11226e90f84bbe038aeaaca66638>>
1111
*/
1212

1313
"use strict";
@@ -203,4 +203,4 @@ exports.useFormState = function (action, initialState, permalink) {
203203
exports.useFormStatus = function () {
204204
return ReactSharedInternals.H.useHostTransitionStatus();
205205
};
206-
exports.version = "19.0.0-native-fb-2a9f4c04-20241122";
206+
exports.version = "19.0.0-native-fb-16d2bbbd-20241203";

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-profiling.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<6333fffa5ab523cc9687eb23bac28b0b>>
10+
* @generated SignedSource<<239a11226e90f84bbe038aeaaca66638>>
1111
*/
1212

1313
"use strict";
@@ -203,4 +203,4 @@ exports.useFormState = function (action, initialState, permalink) {
203203
exports.useFormStatus = function () {
204204
return ReactSharedInternals.H.useHostTransitionStatus();
205205
};
206-
exports.version = "19.0.0-native-fb-2a9f4c04-20241122";
206+
exports.version = "19.0.0-native-fb-16d2bbbd-20241203";

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-dev.js

Lines changed: 198 additions & 178 deletions
Large diffs are not rendered by default.

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-prod.js

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<58c64b2e053860b621da1479bbbcedc7>>
10+
* @generated SignedSource<<f21aeede2360fa78702f7e00ba0891d5>>
1111
*/
1212

1313
/*
@@ -5320,7 +5320,9 @@ function findFirstSuspended(row) {
53205320
if (
53215321
null !== state &&
53225322
((state = state.dehydrated),
5323-
null === state || "$?" === state.data || "$!" === state.data)
5323+
null === state ||
5324+
"$?" === state.data ||
5325+
isSuspenseInstanceFallback(state))
53245326
)
53255327
return node;
53265328
} else if (19 === node.tag && void 0 !== node.memoizedProps.revealOrder) {
@@ -6462,7 +6464,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
64626464
((nextInstance = nextInstance.dehydrated), null !== nextInstance)
64636465
)
64646466
return (
6465-
"$!" === nextInstance.data
6467+
isSuspenseInstanceFallback(nextInstance)
64666468
? (workInProgress.lanes = 16)
64676469
: (workInProgress.lanes = 536870912),
64686470
null
@@ -6572,7 +6574,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
65726574
(workInProgress = showFallback));
65736575
else if (
65746576
(pushPrimaryTreeSuspenseHandler(workInProgress),
6575-
"$!" === nextInstance.data)
6577+
isSuspenseInstanceFallback(nextInstance))
65766578
) {
65776579
JSCompiler_temp =
65786580
nextInstance.nextSibling && nextInstance.nextSibling.dataset;
@@ -6661,7 +6663,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
66616663
null,
66626664
current
66636665
)),
6664-
(nextInstance._reactRetry = workInProgress),
6666+
registerSuspenseInstanceRetry(nextInstance, workInProgress),
66656667
(workInProgress = null))
66666668
: ((renderLanes = JSCompiler_temp$jscomp$0.treeContext),
66676669
(nextHydratableInstance = getNextHydratable(
@@ -12350,20 +12352,20 @@ function extractEvents$1(
1235012352
}
1235112353
}
1235212354
for (
12353-
var i$jscomp$inline_1492 = 0;
12354-
i$jscomp$inline_1492 < simpleEventPluginEvents.length;
12355-
i$jscomp$inline_1492++
12355+
var i$jscomp$inline_1489 = 0;
12356+
i$jscomp$inline_1489 < simpleEventPluginEvents.length;
12357+
i$jscomp$inline_1489++
1235612358
) {
12357-
var eventName$jscomp$inline_1493 =
12358-
simpleEventPluginEvents[i$jscomp$inline_1492],
12359-
domEventName$jscomp$inline_1494 =
12360-
eventName$jscomp$inline_1493.toLowerCase(),
12361-
capitalizedEvent$jscomp$inline_1495 =
12362-
eventName$jscomp$inline_1493[0].toUpperCase() +
12363-
eventName$jscomp$inline_1493.slice(1);
12359+
var eventName$jscomp$inline_1490 =
12360+
simpleEventPluginEvents[i$jscomp$inline_1489],
12361+
domEventName$jscomp$inline_1491 =
12362+
eventName$jscomp$inline_1490.toLowerCase(),
12363+
capitalizedEvent$jscomp$inline_1492 =
12364+
eventName$jscomp$inline_1490[0].toUpperCase() +
12365+
eventName$jscomp$inline_1490.slice(1);
1236412366
registerSimpleEvent(
12365-
domEventName$jscomp$inline_1494,
12366-
"on" + capitalizedEvent$jscomp$inline_1495
12367+
domEventName$jscomp$inline_1491,
12368+
"on" + capitalizedEvent$jscomp$inline_1492
1236712369
);
1236812370
}
1236912371
registerSimpleEvent(ANIMATION_END, "onAnimationEnd");
@@ -14255,6 +14257,24 @@ function canHydrateTextInstance(instance, text, inRootOrSingleton) {
1425514257
}
1425614258
return instance;
1425714259
}
14260+
function isSuspenseInstanceFallback(instance) {
14261+
return (
14262+
"$!" === instance.data ||
14263+
("$?" === instance.data && "complete" === instance.ownerDocument.readyState)
14264+
);
14265+
}
14266+
function registerSuspenseInstanceRetry(instance, callback) {
14267+
var ownerDocument = instance.ownerDocument;
14268+
"complete" !== ownerDocument.readyState &&
14269+
ownerDocument.addEventListener(
14270+
"DOMContentLoaded",
14271+
function () {
14272+
"$?" === instance.data && callback();
14273+
},
14274+
{ once: !0 }
14275+
);
14276+
instance._reactRetry = callback;
14277+
}
1425814278
function getNextHydratable(node) {
1425914279
for (; null != node; node = node.nextSibling) {
1426014280
var nodeType = node.nodeType;
@@ -15836,16 +15856,16 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) {
1583615856
0 === i && attemptExplicitHydrationTarget(target);
1583715857
}
1583815858
};
15839-
var isomorphicReactPackageVersion$jscomp$inline_1735 = React.version;
15859+
var isomorphicReactPackageVersion$jscomp$inline_1732 = React.version;
1584015860
if (
15841-
"19.0.0-native-fb-2a9f4c04-20241122" !==
15842-
isomorphicReactPackageVersion$jscomp$inline_1735
15861+
"19.0.0-native-fb-16d2bbbd-20241203" !==
15862+
isomorphicReactPackageVersion$jscomp$inline_1732
1584315863
)
1584415864
throw Error(
1584515865
formatProdErrorMessage(
1584615866
527,
15847-
isomorphicReactPackageVersion$jscomp$inline_1735,
15848-
"19.0.0-native-fb-2a9f4c04-20241122"
15867+
isomorphicReactPackageVersion$jscomp$inline_1732,
15868+
"19.0.0-native-fb-16d2bbbd-20241203"
1584915869
)
1585015870
);
1585115871
ReactDOMSharedInternals.findDOMNode = function (componentOrElement) {
@@ -15865,25 +15885,25 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) {
1586515885
null === componentOrElement ? null : componentOrElement.stateNode;
1586615886
return componentOrElement;
1586715887
};
15868-
var internals$jscomp$inline_2193 = {
15888+
var internals$jscomp$inline_2187 = {
1586915889
bundleType: 0,
15870-
version: "19.0.0-native-fb-2a9f4c04-20241122",
15890+
version: "19.0.0-native-fb-16d2bbbd-20241203",
1587115891
rendererPackageName: "react-dom",
1587215892
currentDispatcherRef: ReactSharedInternals,
1587315893
findFiberByHostInstance: getClosestInstanceFromNode,
15874-
reconcilerVersion: "19.0.0-native-fb-2a9f4c04-20241122"
15894+
reconcilerVersion: "19.0.0-native-fb-16d2bbbd-20241203"
1587515895
};
1587615896
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
15877-
var hook$jscomp$inline_2194 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
15897+
var hook$jscomp$inline_2188 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
1587815898
if (
15879-
!hook$jscomp$inline_2194.isDisabled &&
15880-
hook$jscomp$inline_2194.supportsFiber
15899+
!hook$jscomp$inline_2188.isDisabled &&
15900+
hook$jscomp$inline_2188.supportsFiber
1588115901
)
1588215902
try {
15883-
(rendererID = hook$jscomp$inline_2194.inject(
15884-
internals$jscomp$inline_2193
15903+
(rendererID = hook$jscomp$inline_2188.inject(
15904+
internals$jscomp$inline_2187
1588515905
)),
15886-
(injectedHook = hook$jscomp$inline_2194);
15906+
(injectedHook = hook$jscomp$inline_2188);
1588715907
} catch (err) {}
1588815908
}
1588915909
exports.createRoot = function (container, options) {
@@ -15975,4 +15995,4 @@ exports.hydrateRoot = function (container, initialChildren, options) {
1597515995
listenToAllSupportedEvents(container);
1597615996
return new ReactDOMHydrationRoot(initialChildren);
1597715997
};
15978-
exports.version = "19.0.0-native-fb-2a9f4c04-20241122";
15998+
exports.version = "19.0.0-native-fb-16d2bbbd-20241203";

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-profiling.js

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<9ea983f5caede381e85e1f5ef2f4e939>>
10+
* @generated SignedSource<<0ebd78a0cf049a802a2b35960edaa452>>
1111
*/
1212

1313
/*
@@ -5472,7 +5472,9 @@ function findFirstSuspended(row) {
54725472
if (
54735473
null !== state &&
54745474
((state = state.dehydrated),
5475-
null === state || "$?" === state.data || "$!" === state.data)
5475+
null === state ||
5476+
"$?" === state.data ||
5477+
isSuspenseInstanceFallback(state))
54765478
)
54775479
return node;
54785480
} else if (19 === node.tag && void 0 !== node.memoizedProps.revealOrder) {
@@ -6633,7 +6635,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
66336635
((nextInstance = nextInstance.dehydrated), null !== nextInstance)
66346636
)
66356637
return (
6636-
"$!" === nextInstance.data
6638+
isSuspenseInstanceFallback(nextInstance)
66376639
? (workInProgress.lanes = 16)
66386640
: (workInProgress.lanes = 536870912),
66396641
null
@@ -6743,7 +6745,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
67436745
(workInProgress = showFallback));
67446746
else if (
67456747
(pushPrimaryTreeSuspenseHandler(workInProgress),
6746-
"$!" === nextInstance.data)
6748+
isSuspenseInstanceFallback(nextInstance))
67476749
) {
67486750
JSCompiler_temp =
67496751
nextInstance.nextSibling && nextInstance.nextSibling.dataset;
@@ -6832,7 +6834,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
68326834
null,
68336835
current
68346836
)),
6835-
(nextInstance._reactRetry = workInProgress),
6837+
registerSuspenseInstanceRetry(nextInstance, workInProgress),
68366838
(workInProgress = null))
68376839
: ((renderLanes = JSCompiler_temp$jscomp$0.treeContext),
68386840
(nextHydratableInstance = getNextHydratable(
@@ -12995,20 +12997,20 @@ function extractEvents$1(
1299512997
}
1299612998
}
1299712999
for (
12998-
var i$jscomp$inline_1580 = 0;
12999-
i$jscomp$inline_1580 < simpleEventPluginEvents.length;
13000-
i$jscomp$inline_1580++
13000+
var i$jscomp$inline_1577 = 0;
13001+
i$jscomp$inline_1577 < simpleEventPluginEvents.length;
13002+
i$jscomp$inline_1577++
1300113003
) {
13002-
var eventName$jscomp$inline_1581 =
13003-
simpleEventPluginEvents[i$jscomp$inline_1580],
13004-
domEventName$jscomp$inline_1582 =
13005-
eventName$jscomp$inline_1581.toLowerCase(),
13006-
capitalizedEvent$jscomp$inline_1583 =
13007-
eventName$jscomp$inline_1581[0].toUpperCase() +
13008-
eventName$jscomp$inline_1581.slice(1);
13004+
var eventName$jscomp$inline_1578 =
13005+
simpleEventPluginEvents[i$jscomp$inline_1577],
13006+
domEventName$jscomp$inline_1579 =
13007+
eventName$jscomp$inline_1578.toLowerCase(),
13008+
capitalizedEvent$jscomp$inline_1580 =
13009+
eventName$jscomp$inline_1578[0].toUpperCase() +
13010+
eventName$jscomp$inline_1578.slice(1);
1300913011
registerSimpleEvent(
13010-
domEventName$jscomp$inline_1582,
13011-
"on" + capitalizedEvent$jscomp$inline_1583
13012+
domEventName$jscomp$inline_1579,
13013+
"on" + capitalizedEvent$jscomp$inline_1580
1301213014
);
1301313015
}
1301413016
registerSimpleEvent(ANIMATION_END, "onAnimationEnd");
@@ -14900,6 +14902,24 @@ function canHydrateTextInstance(instance, text, inRootOrSingleton) {
1490014902
}
1490114903
return instance;
1490214904
}
14905+
function isSuspenseInstanceFallback(instance) {
14906+
return (
14907+
"$!" === instance.data ||
14908+
("$?" === instance.data && "complete" === instance.ownerDocument.readyState)
14909+
);
14910+
}
14911+
function registerSuspenseInstanceRetry(instance, callback) {
14912+
var ownerDocument = instance.ownerDocument;
14913+
"complete" !== ownerDocument.readyState &&
14914+
ownerDocument.addEventListener(
14915+
"DOMContentLoaded",
14916+
function () {
14917+
"$?" === instance.data && callback();
14918+
},
14919+
{ once: !0 }
14920+
);
14921+
instance._reactRetry = callback;
14922+
}
1490314923
function getNextHydratable(node) {
1490414924
for (; null != node; node = node.nextSibling) {
1490514925
var nodeType = node.nodeType;
@@ -16489,16 +16509,16 @@ ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = function (target) {
1648916509
0 === i && attemptExplicitHydrationTarget(target);
1649016510
}
1649116511
};
16492-
var isomorphicReactPackageVersion$jscomp$inline_1825 = React.version;
16512+
var isomorphicReactPackageVersion$jscomp$inline_1822 = React.version;
1649316513
if (
16494-
"19.0.0-native-fb-2a9f4c04-20241122" !==
16495-
isomorphicReactPackageVersion$jscomp$inline_1825
16514+
"19.0.0-native-fb-16d2bbbd-20241203" !==
16515+
isomorphicReactPackageVersion$jscomp$inline_1822
1649616516
)
1649716517
throw Error(
1649816518
formatProdErrorMessage(
1649916519
527,
16500-
isomorphicReactPackageVersion$jscomp$inline_1825,
16501-
"19.0.0-native-fb-2a9f4c04-20241122"
16520+
isomorphicReactPackageVersion$jscomp$inline_1822,
16521+
"19.0.0-native-fb-16d2bbbd-20241203"
1650216522
)
1650316523
);
1650416524
ReactDOMSharedInternals.findDOMNode = function (componentOrElement) {
@@ -16518,13 +16538,13 @@ ReactDOMSharedInternals.findDOMNode = function (componentOrElement) {
1651816538
null === componentOrElement ? null : componentOrElement.stateNode;
1651916539
return componentOrElement;
1652016540
};
16521-
var internals$jscomp$inline_1832 = {
16541+
var internals$jscomp$inline_1829 = {
1652216542
bundleType: 0,
16523-
version: "19.0.0-native-fb-2a9f4c04-20241122",
16543+
version: "19.0.0-native-fb-16d2bbbd-20241203",
1652416544
rendererPackageName: "react-dom",
1652516545
currentDispatcherRef: ReactSharedInternals,
1652616546
findFiberByHostInstance: getClosestInstanceFromNode,
16527-
reconcilerVersion: "19.0.0-native-fb-2a9f4c04-20241122",
16547+
reconcilerVersion: "19.0.0-native-fb-16d2bbbd-20241203",
1652816548
getLaneLabelMap: function () {
1652916549
for (
1653016550
var map = new Map(), lane = 1, index$292 = 0;
@@ -16542,16 +16562,16 @@ var internals$jscomp$inline_1832 = {
1654216562
}
1654316563
};
1654416564
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
16545-
var hook$jscomp$inline_2245 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
16565+
var hook$jscomp$inline_2239 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
1654616566
if (
16547-
!hook$jscomp$inline_2245.isDisabled &&
16548-
hook$jscomp$inline_2245.supportsFiber
16567+
!hook$jscomp$inline_2239.isDisabled &&
16568+
hook$jscomp$inline_2239.supportsFiber
1654916569
)
1655016570
try {
16551-
(rendererID = hook$jscomp$inline_2245.inject(
16552-
internals$jscomp$inline_1832
16571+
(rendererID = hook$jscomp$inline_2239.inject(
16572+
internals$jscomp$inline_1829
1655316573
)),
16554-
(injectedHook = hook$jscomp$inline_2245);
16574+
(injectedHook = hook$jscomp$inline_2239);
1655516575
} catch (err) {}
1655616576
}
1655716577
exports.createRoot = function (container, options) {
@@ -16643,4 +16663,4 @@ exports.hydrateRoot = function (container, initialChildren, options) {
1664316663
listenToAllSupportedEvents(container);
1664416664
return new ReactDOMHydrationRoot(initialChildren);
1664516665
};
16646-
exports.version = "19.0.0-native-fb-2a9f4c04-20241122";
16666+
exports.version = "19.0.0-native-fb-16d2bbbd-20241203";

0 commit comments

Comments
 (0)