Skip to content

Commit 192a05b

Browse files
committed
Bugfix: SuspenseList incorrectly forces a fallback (#26453)
Fixes a bug in SuspenseList that @kassens found when deploying React to Meta. In some scenarios, SuspenseList would force the fallback of a deeply nested Suspense boundary into fallback mode, which should never happen under any circumstances — SuspenseList should only affect the nearest descendent Suspense boundaries, without going deeper. The cause was that the internal ForceSuspenseFallback context flag was not being properly reset when it reached the nearest Suspense boundary. It should only be propagated shallowly. We didn't discover this earlier because the scenario where it happens is not that common. To trigger the bug, you need to insert a new Suspense boundary into an already-mounted row of the list. But often when a new Suspense boundary is rendered, it suspends and shows a fallback, anyway, because its content hasn't loaded yet. Another reason we didn't discover this earlier is because there was another bug that was accidentally masking it, which was fixed by #25922. When that fix landed, it revealed this bug. The SuspenseList implementation is complicated but I'm not too concerned with the current messiness. It's an experimental API, and we intend to release it soon, but there are some known flaws and missing features that we need to address first regardless. We'll likely end up rewriting most of it. Co-authored-by: Jan Kassens <[email protected]> DiffTrain build for [51a7c45](51a7c45)
1 parent 64facb3 commit 192a05b

18 files changed

+539
-339
lines changed

compiled/facebook-www/REVISION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
afb3d51dc6310f0dbeffdd303eb3c6895e6f7db0
1+
51a7c45f8799cab903693fcfdd305ce84ba15273

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ if (
2727
}
2828
"use strict";
2929

30-
var ReactVersion = "18.3.0-www-modern-b3a4de20";
30+
var ReactVersion = "18.3.0-www-modern-e3dcc95d";
3131

3232
// ATTENTION
3333
// When adding new symbols to this file,

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
6969
return self;
7070
}
7171

72-
var ReactVersion = "18.3.0-www-classic-ac5037f5";
72+
var ReactVersion = "18.3.0-www-classic-7d98bb40";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -7166,7 +7166,14 @@ function getShellBoundary() {
71667166
function pushPrimaryTreeSuspenseHandler(handler) {
71677167
// TODO: Pass as argument
71687168
var current = handler.alternate;
7169-
var props = handler.pendingProps; // Experimental feature: Some Suspense boundaries are marked as having an
7169+
var props = handler.pendingProps; // Shallow Suspense context fields, like ForceSuspenseFallback, should only be
7170+
// propagated a single level. For example, when ForceSuspenseFallback is set,
7171+
// it should only force the nearest Suspense boundary into fallback mode.
7172+
7173+
pushSuspenseListContext(
7174+
handler,
7175+
setDefaultShallowSuspenseListContext(suspenseStackCursor.current)
7176+
); // Experimental feature: Some Suspense boundaries are marked as having an
71707177
// undesirable fallback state. These have special behavior where we only
71717178
// activate the fallback if there's no other boundary on the stack that we can
71727179
// use instead.
@@ -7218,6 +7225,11 @@ function pushFallbackTreeSuspenseHandler(fiber) {
72187225
}
72197226
function pushOffscreenSuspenseHandler(fiber) {
72207227
if (fiber.tag === OffscreenComponent) {
7228+
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
7229+
// Reuse the current value on the stack.
7230+
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
7231+
// into separate functions for Suspense and Offscreen.
7232+
pushSuspenseListContext(fiber, suspenseStackCursor.current);
72217233
push(suspenseHandlerStackCursor, fiber, fiber);
72227234

72237235
if (shellBoundary !== null);
@@ -7240,6 +7252,7 @@ function pushOffscreenSuspenseHandler(fiber) {
72407252
}
72417253
}
72427254
function reuseSuspenseHandlerOnStack(fiber) {
7255+
pushSuspenseListContext(fiber, suspenseStackCursor.current);
72437256
push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
72447257
}
72457258
function getSuspenseHandler() {
@@ -7252,6 +7265,8 @@ function popSuspenseHandler(fiber) {
72527265
// Popping back into the shell.
72537266
shellBoundary = null;
72547267
}
7268+
7269+
popSuspenseListContext(fiber);
72557270
} // SuspenseList context
72567271
// TODO: Move to a separate module? We may change the SuspenseList
72577272
// implementation to hide/show in the commit phase, anyway.
@@ -14551,6 +14566,8 @@ function shouldRemainOnFallback(current, workInProgress, renderLanes) {
1455114566
// If we're already showing a fallback, there are cases where we need to
1455214567
// remain on that fallback regardless of whether the content has resolved.
1455314568
// For example, SuspenseList coordinates when nested content appears.
14569+
// TODO: For compatibility with offscreen prerendering, this should also check
14570+
// whether the current fiber (if it exists) was visible in the previous tree.
1455414571
if (current !== null) {
1455514572
var suspenseState = current.memoizedState;
1455614573

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

+19-2
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function _assertThisInitialized(self) {
6969
return self;
7070
}
7171

72-
var ReactVersion = "18.3.0-www-modern-d114403d";
72+
var ReactVersion = "18.3.0-www-modern-34dd0fe8";
7373

7474
var LegacyRoot = 0;
7575
var ConcurrentRoot = 1;
@@ -6922,7 +6922,14 @@ function getShellBoundary() {
69226922
function pushPrimaryTreeSuspenseHandler(handler) {
69236923
// TODO: Pass as argument
69246924
var current = handler.alternate;
6925-
var props = handler.pendingProps; // Experimental feature: Some Suspense boundaries are marked as having an
6925+
var props = handler.pendingProps; // Shallow Suspense context fields, like ForceSuspenseFallback, should only be
6926+
// propagated a single level. For example, when ForceSuspenseFallback is set,
6927+
// it should only force the nearest Suspense boundary into fallback mode.
6928+
6929+
pushSuspenseListContext(
6930+
handler,
6931+
setDefaultShallowSuspenseListContext(suspenseStackCursor.current)
6932+
); // Experimental feature: Some Suspense boundaries are marked as having an
69266933
// undesirable fallback state. These have special behavior where we only
69276934
// activate the fallback if there's no other boundary on the stack that we can
69286935
// use instead.
@@ -6974,6 +6981,11 @@ function pushFallbackTreeSuspenseHandler(fiber) {
69746981
}
69756982
function pushOffscreenSuspenseHandler(fiber) {
69766983
if (fiber.tag === OffscreenComponent) {
6984+
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
6985+
// Reuse the current value on the stack.
6986+
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
6987+
// into separate functions for Suspense and Offscreen.
6988+
pushSuspenseListContext(fiber, suspenseStackCursor.current);
69776989
push(suspenseHandlerStackCursor, fiber, fiber);
69786990

69796991
if (shellBoundary !== null);
@@ -6996,6 +7008,7 @@ function pushOffscreenSuspenseHandler(fiber) {
69967008
}
69977009
}
69987010
function reuseSuspenseHandlerOnStack(fiber) {
7011+
pushSuspenseListContext(fiber, suspenseStackCursor.current);
69997012
push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
70007013
}
70017014
function getSuspenseHandler() {
@@ -7008,6 +7021,8 @@ function popSuspenseHandler(fiber) {
70087021
// Popping back into the shell.
70097022
shellBoundary = null;
70107023
}
7024+
7025+
popSuspenseListContext(fiber);
70117026
} // SuspenseList context
70127027
// TODO: Move to a separate module? We may change the SuspenseList
70137028
// implementation to hide/show in the commit phase, anyway.
@@ -14246,6 +14261,8 @@ function shouldRemainOnFallback(current, workInProgress, renderLanes) {
1424614261
// If we're already showing a fallback, there are cases where we need to
1424714262
// remain on that fallback regardless of whether the content has resolved.
1424814263
// For example, SuspenseList coordinates when nested content appears.
14264+
// TODO: For compatibility with offscreen prerendering, this should also check
14265+
// whether the current fiber (if it exists) was visible in the previous tree.
1424914266
if (current !== null) {
1425014267
var suspenseState = current.memoizedState;
1425114268

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

+34-26
Original file line numberDiff line numberDiff line change
@@ -2248,8 +2248,10 @@ function popHiddenContext() {
22482248
var suspenseHandlerStackCursor = createCursor(null),
22492249
shellBoundary = null;
22502250
function pushPrimaryTreeSuspenseHandler(handler) {
2251-
var current = handler.alternate;
2252-
!0 !== handler.pendingProps.unstable_avoidThisFallback ||
2251+
var current = handler.alternate,
2252+
props = handler.pendingProps;
2253+
push(suspenseStackCursor, suspenseStackCursor.current & 1);
2254+
!0 !== props.unstable_avoidThisFallback ||
22532255
(null !== current && null === currentTreeHiddenStackCursor.current)
22542256
? (push(suspenseHandlerStackCursor, handler),
22552257
null === shellBoundary &&
@@ -2262,20 +2264,26 @@ function pushPrimaryTreeSuspenseHandler(handler) {
22622264
}
22632265
function pushOffscreenSuspenseHandler(fiber) {
22642266
if (22 === fiber.tag) {
2265-
if ((push(suspenseHandlerStackCursor, fiber), null === shellBoundary)) {
2267+
if (
2268+
(push(suspenseStackCursor, suspenseStackCursor.current),
2269+
push(suspenseHandlerStackCursor, fiber),
2270+
null === shellBoundary)
2271+
) {
22662272
var current = fiber.alternate;
22672273
null !== current &&
22682274
null !== current.memoizedState &&
22692275
(shellBoundary = fiber);
22702276
}
2271-
} else reuseSuspenseHandlerOnStack();
2277+
} else reuseSuspenseHandlerOnStack(fiber);
22722278
}
22732279
function reuseSuspenseHandlerOnStack() {
2280+
push(suspenseStackCursor, suspenseStackCursor.current);
22742281
push(suspenseHandlerStackCursor, suspenseHandlerStackCursor.current);
22752282
}
22762283
function popSuspenseHandler(fiber) {
22772284
pop(suspenseHandlerStackCursor);
22782285
shellBoundary === fiber && (shellBoundary = null);
2286+
pop(suspenseStackCursor);
22792287
}
22802288
var suspenseStackCursor = createCursor(0);
22812289
function findFirstSuspended(row) {
@@ -3857,12 +3865,12 @@ function updateOffscreenComponent(current, workInProgress, renderLanes) {
38573865
}
38583866
pushTransition(workInProgress, nextProps, nextIsDetached);
38593867
pushHiddenContext(workInProgress, prevState);
3860-
reuseSuspenseHandlerOnStack();
3868+
reuseSuspenseHandlerOnStack(workInProgress);
38613869
workInProgress.memoizedState = null;
38623870
} else
38633871
null !== current && pushTransition(workInProgress, null, null),
38643872
reuseHiddenContextOnStack(),
3865-
reuseSuspenseHandlerOnStack();
3873+
reuseSuspenseHandlerOnStack(workInProgress);
38663874
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
38673875
return workInProgress.child;
38683876
}
@@ -4232,7 +4240,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
42324240
didSuspend = nextProps.fallback;
42334241
if (showFallback)
42344242
return (
4235-
reuseSuspenseHandlerOnStack(),
4243+
reuseSuspenseHandlerOnStack(workInProgress),
42364244
(current = mountSuspenseFallbackChildren(
42374245
workInProgress,
42384246
current,
@@ -4263,7 +4271,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
42634271
);
42644272
if ("number" === typeof nextProps.unstable_expectedLoadTime)
42654273
return (
4266-
reuseSuspenseHandlerOnStack(),
4274+
reuseSuspenseHandlerOnStack(workInProgress),
42674275
(current = mountSuspenseFallbackChildren(
42684276
workInProgress,
42694277
current,
@@ -4294,7 +4302,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
42944302
);
42954303
}
42964304
if (showFallback) {
4297-
reuseSuspenseHandlerOnStack();
4305+
reuseSuspenseHandlerOnStack(workInProgress);
42984306
showFallback = nextProps.fallback;
42994307
didSuspend = workInProgress.mode;
43004308
JSCompiler_temp = current.child;
@@ -4469,12 +4477,12 @@ function updateDehydratedSuspenseComponent(
44694477
);
44704478
if (null !== workInProgress.memoizedState)
44714479
return (
4472-
reuseSuspenseHandlerOnStack(),
4480+
reuseSuspenseHandlerOnStack(workInProgress),
44734481
(workInProgress.child = current.child),
44744482
(workInProgress.flags |= 128),
44754483
null
44764484
);
4477-
reuseSuspenseHandlerOnStack();
4485+
reuseSuspenseHandlerOnStack(workInProgress);
44784486
suspenseState = nextProps.fallback;
44794487
didSuspend = workInProgress.mode;
44804488
nextProps = createFiberFromOffscreen(
@@ -9957,19 +9965,19 @@ var slice = Array.prototype.slice,
99579965
};
99589966
return Text;
99599967
})(React.Component),
9960-
devToolsConfig$jscomp$inline_1157 = {
9968+
devToolsConfig$jscomp$inline_1168 = {
99619969
findFiberByHostInstance: function () {
99629970
return null;
99639971
},
99649972
bundleType: 0,
9965-
version: "18.3.0-www-classic-f03f0ae3",
9973+
version: "18.3.0-www-classic-52e77af5",
99669974
rendererPackageName: "react-art"
99679975
};
9968-
var internals$jscomp$inline_1332 = {
9969-
bundleType: devToolsConfig$jscomp$inline_1157.bundleType,
9970-
version: devToolsConfig$jscomp$inline_1157.version,
9971-
rendererPackageName: devToolsConfig$jscomp$inline_1157.rendererPackageName,
9972-
rendererConfig: devToolsConfig$jscomp$inline_1157.rendererConfig,
9976+
var internals$jscomp$inline_1343 = {
9977+
bundleType: devToolsConfig$jscomp$inline_1168.bundleType,
9978+
version: devToolsConfig$jscomp$inline_1168.version,
9979+
rendererPackageName: devToolsConfig$jscomp$inline_1168.rendererPackageName,
9980+
rendererConfig: devToolsConfig$jscomp$inline_1168.rendererConfig,
99739981
overrideHookState: null,
99749982
overrideHookStateDeletePath: null,
99759983
overrideHookStateRenamePath: null,
@@ -9986,26 +9994,26 @@ var internals$jscomp$inline_1332 = {
99869994
return null === fiber ? null : fiber.stateNode;
99879995
},
99889996
findFiberByHostInstance:
9989-
devToolsConfig$jscomp$inline_1157.findFiberByHostInstance ||
9997+
devToolsConfig$jscomp$inline_1168.findFiberByHostInstance ||
99909998
emptyFindFiberByHostInstance,
99919999
findHostInstancesForRefresh: null,
999210000
scheduleRefresh: null,
999310001
scheduleRoot: null,
999410002
setRefreshHandler: null,
999510003
getCurrentFiber: null,
9996-
reconcilerVersion: "18.3.0-www-classic-f03f0ae3"
10004+
reconcilerVersion: "18.3.0-www-classic-52e77af5"
999710005
};
999810006
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
9999-
var hook$jscomp$inline_1333 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
10007+
var hook$jscomp$inline_1344 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
1000010008
if (
10001-
!hook$jscomp$inline_1333.isDisabled &&
10002-
hook$jscomp$inline_1333.supportsFiber
10009+
!hook$jscomp$inline_1344.isDisabled &&
10010+
hook$jscomp$inline_1344.supportsFiber
1000310011
)
1000410012
try {
10005-
(rendererID = hook$jscomp$inline_1333.inject(
10006-
internals$jscomp$inline_1332
10013+
(rendererID = hook$jscomp$inline_1344.inject(
10014+
internals$jscomp$inline_1343
1000710015
)),
10008-
(injectedHook = hook$jscomp$inline_1333);
10016+
(injectedHook = hook$jscomp$inline_1344);
1000910017
} catch (err) {}
1001010018
}
1001110019
var Path = Mode$1.Path;

0 commit comments

Comments
 (0)