Skip to content

Commit 2a91152

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 commit 51a7c45.
1 parent 7ec3959 commit 2a91152

File tree

13 files changed

+232
-135
lines changed

13 files changed

+232
-135
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-dev.js

+18-1
Original file line numberDiff line numberDiff line change
@@ -5886,6 +5886,13 @@ function getShellBoundary() {
58865886
function pushPrimaryTreeSuspenseHandler(handler) {
58875887
// TODO: Pass as argument
58885888
var current = handler.alternate;
5889+
// propagated a single level. For example, when ForceSuspenseFallback is set,
5890+
// it should only force the nearest Suspense boundary into fallback mode.
5891+
5892+
pushSuspenseListContext(
5893+
handler,
5894+
setDefaultShallowSuspenseListContext(suspenseStackCursor.current)
5895+
); // Experimental feature: Some Suspense boundaries are marked as having an
58895896
// to push a nested Suspense handler, because it will get replaced by the
58905897
// outer fallback, anyway. Consider this as a future optimization.
58915898

@@ -5913,6 +5920,11 @@ function pushFallbackTreeSuspenseHandler(fiber) {
59135920
}
59145921
function pushOffscreenSuspenseHandler(fiber) {
59155922
if (fiber.tag === OffscreenComponent) {
5923+
// A SuspenseList context is only pushed here to avoid a push/pop mismatch.
5924+
// Reuse the current value on the stack.
5925+
// TODO: We can avoid needing to push here by by forking popSuspenseHandler
5926+
// into separate functions for Suspense and Offscreen.
5927+
pushSuspenseListContext(fiber, suspenseStackCursor.current);
59165928
push(suspenseHandlerStackCursor, fiber, fiber);
59175929

59185930
if (shellBoundary !== null);
@@ -5935,6 +5947,7 @@ function pushOffscreenSuspenseHandler(fiber) {
59355947
}
59365948
}
59375949
function reuseSuspenseHandlerOnStack(fiber) {
5950+
pushSuspenseListContext(fiber, suspenseStackCursor.current);
59385951
push(suspenseHandlerStackCursor, getSuspenseHandler(), fiber);
59395952
}
59405953
function getSuspenseHandler() {
@@ -5947,6 +5960,8 @@ function popSuspenseHandler(fiber) {
59475960
// Popping back into the shell.
59485961
shellBoundary = null;
59495962
}
5963+
5964+
popSuspenseListContext(fiber);
59505965
} // SuspenseList context
59515966
// TODO: Move to a separate module? We may change the SuspenseList
59525967
// implementation to hide/show in the commit phase, anyway.
@@ -12176,6 +12191,8 @@ function shouldRemainOnFallback(current, workInProgress, renderLanes) {
1217612191
// If we're already showing a fallback, there are cases where we need to
1217712192
// remain on that fallback regardless of whether the content has resolved.
1217812193
// For example, SuspenseList coordinates when nested content appears.
12194+
// TODO: For compatibility with offscreen prerendering, this should also check
12195+
// whether the current fiber (if it exists) was visible in the previous tree.
1217912196
if (current !== null) {
1218012197
var suspenseState = current.memoizedState;
1218112198

@@ -23632,7 +23649,7 @@ function createFiberRoot(
2363223649
return root;
2363323650
}
2363423651

23635-
var ReactVersion = "18.3.0-next-afb3d51dc-20230322";
23652+
var ReactVersion = "18.3.0-next-51a7c45f8-20230322";
2363623653

2363723654
// Might add PROFILE later.
2363823655

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-prod.js

+30-23
Original file line numberDiff line numberDiff line change
@@ -1950,6 +1950,7 @@ var suspenseHandlerStackCursor = createCursor(null),
19501950
shellBoundary = null;
19511951
function pushPrimaryTreeSuspenseHandler(handler) {
19521952
var current = handler.alternate;
1953+
push(suspenseStackCursor, suspenseStackCursor.current & 1);
19531954
push(suspenseHandlerStackCursor, handler);
19541955
null === shellBoundary &&
19551956
(null === current || null !== currentTreeHiddenStackCursor.current
@@ -1958,20 +1959,26 @@ function pushPrimaryTreeSuspenseHandler(handler) {
19581959
}
19591960
function pushOffscreenSuspenseHandler(fiber) {
19601961
if (22 === fiber.tag) {
1961-
if ((push(suspenseHandlerStackCursor, fiber), null === shellBoundary)) {
1962+
if (
1963+
(push(suspenseStackCursor, suspenseStackCursor.current),
1964+
push(suspenseHandlerStackCursor, fiber),
1965+
null === shellBoundary)
1966+
) {
19621967
var current = fiber.alternate;
19631968
null !== current &&
19641969
null !== current.memoizedState &&
19651970
(shellBoundary = fiber);
19661971
}
1967-
} else reuseSuspenseHandlerOnStack();
1972+
} else reuseSuspenseHandlerOnStack(fiber);
19681973
}
19691974
function reuseSuspenseHandlerOnStack() {
1975+
push(suspenseStackCursor, suspenseStackCursor.current);
19701976
push(suspenseHandlerStackCursor, suspenseHandlerStackCursor.current);
19711977
}
19721978
function popSuspenseHandler(fiber) {
19731979
pop(suspenseHandlerStackCursor);
19741980
shellBoundary === fiber && (shellBoundary = null);
1981+
pop(suspenseStackCursor);
19751982
}
19761983
var suspenseStackCursor = createCursor(0);
19771984
function findFirstSuspended(row) {
@@ -3220,11 +3227,11 @@ function updateOffscreenComponent(current, workInProgress, renderLanes) {
32203227
null !== prevState
32213228
? (pushTransition(workInProgress, prevState.cachePool),
32223229
pushHiddenContext(workInProgress, prevState),
3223-
reuseSuspenseHandlerOnStack(),
3230+
reuseSuspenseHandlerOnStack(workInProgress),
32243231
(workInProgress.memoizedState = null))
32253232
: (null !== current && pushTransition(workInProgress, null),
32263233
reuseHiddenContextOnStack(),
3227-
reuseSuspenseHandlerOnStack());
3234+
reuseSuspenseHandlerOnStack(workInProgress));
32283235
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
32293236
return workInProgress.child;
32303237
}
@@ -3579,7 +3586,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
35793586
current = nextProps.fallback;
35803587
if (showFallback)
35813588
return (
3582-
reuseSuspenseHandlerOnStack(),
3589+
reuseSuspenseHandlerOnStack(workInProgress),
35833590
(nextProps = workInProgress.mode),
35843591
(showFallback = workInProgress.child),
35853592
(didSuspend = { mode: "hidden", children: didSuspend }),
@@ -3625,7 +3632,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
36253632
);
36263633
}
36273634
if (showFallback) {
3628-
reuseSuspenseHandlerOnStack();
3635+
reuseSuspenseHandlerOnStack(workInProgress);
36293636
showFallback = nextProps.fallback;
36303637
didSuspend = workInProgress.mode;
36313638
JSCompiler_temp = current.child;
@@ -3751,12 +3758,12 @@ function updateDehydratedSuspenseComponent(
37513758
);
37523759
if (null !== workInProgress.memoizedState)
37533760
return (
3754-
reuseSuspenseHandlerOnStack(),
3761+
reuseSuspenseHandlerOnStack(workInProgress),
37553762
(workInProgress.child = current.child),
37563763
(workInProgress.flags |= 128),
37573764
null
37583765
);
3759-
reuseSuspenseHandlerOnStack();
3766+
reuseSuspenseHandlerOnStack(workInProgress);
37603767
suspenseState = nextProps.fallback;
37613768
didSuspend = workInProgress.mode;
37623769
nextProps = createFiberFromOffscreen(
@@ -8558,19 +8565,19 @@ function wrapFiber(fiber) {
85588565
fiberToWrapper.set(fiber, wrapper));
85598566
return wrapper;
85608567
}
8561-
var devToolsConfig$jscomp$inline_1018 = {
8568+
var devToolsConfig$jscomp$inline_1029 = {
85628569
findFiberByHostInstance: function () {
85638570
throw Error("TestRenderer does not support findFiberByHostInstance()");
85648571
},
85658572
bundleType: 0,
8566-
version: "18.3.0-next-afb3d51dc-20230322",
8573+
version: "18.3.0-next-51a7c45f8-20230322",
85678574
rendererPackageName: "react-test-renderer"
85688575
};
8569-
var internals$jscomp$inline_1206 = {
8570-
bundleType: devToolsConfig$jscomp$inline_1018.bundleType,
8571-
version: devToolsConfig$jscomp$inline_1018.version,
8572-
rendererPackageName: devToolsConfig$jscomp$inline_1018.rendererPackageName,
8573-
rendererConfig: devToolsConfig$jscomp$inline_1018.rendererConfig,
8576+
var internals$jscomp$inline_1217 = {
8577+
bundleType: devToolsConfig$jscomp$inline_1029.bundleType,
8578+
version: devToolsConfig$jscomp$inline_1029.version,
8579+
rendererPackageName: devToolsConfig$jscomp$inline_1029.rendererPackageName,
8580+
rendererConfig: devToolsConfig$jscomp$inline_1029.rendererConfig,
85748581
overrideHookState: null,
85758582
overrideHookStateDeletePath: null,
85768583
overrideHookStateRenamePath: null,
@@ -8587,26 +8594,26 @@ var internals$jscomp$inline_1206 = {
85878594
return null === fiber ? null : fiber.stateNode;
85888595
},
85898596
findFiberByHostInstance:
8590-
devToolsConfig$jscomp$inline_1018.findFiberByHostInstance ||
8597+
devToolsConfig$jscomp$inline_1029.findFiberByHostInstance ||
85918598
emptyFindFiberByHostInstance,
85928599
findHostInstancesForRefresh: null,
85938600
scheduleRefresh: null,
85948601
scheduleRoot: null,
85958602
setRefreshHandler: null,
85968603
getCurrentFiber: null,
8597-
reconcilerVersion: "18.3.0-next-afb3d51dc-20230322"
8604+
reconcilerVersion: "18.3.0-next-51a7c45f8-20230322"
85988605
};
85998606
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
8600-
var hook$jscomp$inline_1207 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
8607+
var hook$jscomp$inline_1218 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
86018608
if (
8602-
!hook$jscomp$inline_1207.isDisabled &&
8603-
hook$jscomp$inline_1207.supportsFiber
8609+
!hook$jscomp$inline_1218.isDisabled &&
8610+
hook$jscomp$inline_1218.supportsFiber
86048611
)
86058612
try {
8606-
(rendererID = hook$jscomp$inline_1207.inject(
8607-
internals$jscomp$inline_1206
8613+
(rendererID = hook$jscomp$inline_1218.inject(
8614+
internals$jscomp$inline_1217
86088615
)),
8609-
(injectedHook = hook$jscomp$inline_1207);
8616+
(injectedHook = hook$jscomp$inline_1218);
86108617
} catch (err) {}
86118618
}
86128619
exports._Scheduler = Scheduler;

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react-test-renderer/cjs/ReactTestRenderer-profiling.js

+30-23
Original file line numberDiff line numberDiff line change
@@ -1968,6 +1968,7 @@ var suspenseHandlerStackCursor = createCursor(null),
19681968
shellBoundary = null;
19691969
function pushPrimaryTreeSuspenseHandler(handler) {
19701970
var current = handler.alternate;
1971+
push(suspenseStackCursor, suspenseStackCursor.current & 1);
19711972
push(suspenseHandlerStackCursor, handler);
19721973
null === shellBoundary &&
19731974
(null === current || null !== currentTreeHiddenStackCursor.current
@@ -1976,20 +1977,26 @@ function pushPrimaryTreeSuspenseHandler(handler) {
19761977
}
19771978
function pushOffscreenSuspenseHandler(fiber) {
19781979
if (22 === fiber.tag) {
1979-
if ((push(suspenseHandlerStackCursor, fiber), null === shellBoundary)) {
1980+
if (
1981+
(push(suspenseStackCursor, suspenseStackCursor.current),
1982+
push(suspenseHandlerStackCursor, fiber),
1983+
null === shellBoundary)
1984+
) {
19801985
var current = fiber.alternate;
19811986
null !== current &&
19821987
null !== current.memoizedState &&
19831988
(shellBoundary = fiber);
19841989
}
1985-
} else reuseSuspenseHandlerOnStack();
1990+
} else reuseSuspenseHandlerOnStack(fiber);
19861991
}
19871992
function reuseSuspenseHandlerOnStack() {
1993+
push(suspenseStackCursor, suspenseStackCursor.current);
19881994
push(suspenseHandlerStackCursor, suspenseHandlerStackCursor.current);
19891995
}
19901996
function popSuspenseHandler(fiber) {
19911997
pop(suspenseHandlerStackCursor);
19921998
shellBoundary === fiber && (shellBoundary = null);
1999+
pop(suspenseStackCursor);
19932000
}
19942001
var suspenseStackCursor = createCursor(0);
19952002
function findFirstSuspended(row) {
@@ -3300,11 +3307,11 @@ function updateOffscreenComponent(current, workInProgress, renderLanes) {
33003307
null !== prevState
33013308
? (pushTransition(workInProgress, prevState.cachePool),
33023309
pushHiddenContext(workInProgress, prevState),
3303-
reuseSuspenseHandlerOnStack(),
3310+
reuseSuspenseHandlerOnStack(workInProgress),
33043311
(workInProgress.memoizedState = null))
33053312
: (null !== current && pushTransition(workInProgress, null),
33063313
reuseHiddenContextOnStack(),
3307-
reuseSuspenseHandlerOnStack());
3314+
reuseSuspenseHandlerOnStack(workInProgress));
33083315
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
33093316
return workInProgress.child;
33103317
}
@@ -3663,7 +3670,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
36633670
current = nextProps.fallback;
36643671
if (showFallback)
36653672
return (
3666-
reuseSuspenseHandlerOnStack(),
3673+
reuseSuspenseHandlerOnStack(workInProgress),
36673674
(nextProps = workInProgress.mode),
36683675
(showFallback = workInProgress.child),
36693676
(didSuspend = { mode: "hidden", children: didSuspend }),
@@ -3714,7 +3721,7 @@ function updateSuspenseComponent(current, workInProgress, renderLanes) {
37143721
);
37153722
}
37163723
if (showFallback) {
3717-
reuseSuspenseHandlerOnStack();
3724+
reuseSuspenseHandlerOnStack(workInProgress);
37183725
showFallback = nextProps.fallback;
37193726
didSuspend = workInProgress.mode;
37203727
JSCompiler_temp = current.child;
@@ -3845,12 +3852,12 @@ function updateDehydratedSuspenseComponent(
38453852
);
38463853
if (null !== workInProgress.memoizedState)
38473854
return (
3848-
reuseSuspenseHandlerOnStack(),
3855+
reuseSuspenseHandlerOnStack(workInProgress),
38493856
(workInProgress.child = current.child),
38503857
(workInProgress.flags |= 128),
38513858
null
38523859
);
3853-
reuseSuspenseHandlerOnStack();
3860+
reuseSuspenseHandlerOnStack(workInProgress);
38543861
suspenseState = nextProps.fallback;
38553862
didSuspend = workInProgress.mode;
38563863
nextProps = createFiberFromOffscreen(
@@ -8983,19 +8990,19 @@ function wrapFiber(fiber) {
89838990
fiberToWrapper.set(fiber, wrapper));
89848991
return wrapper;
89858992
}
8986-
var devToolsConfig$jscomp$inline_1061 = {
8993+
var devToolsConfig$jscomp$inline_1072 = {
89878994
findFiberByHostInstance: function () {
89888995
throw Error("TestRenderer does not support findFiberByHostInstance()");
89898996
},
89908997
bundleType: 0,
8991-
version: "18.3.0-next-afb3d51dc-20230322",
8998+
version: "18.3.0-next-51a7c45f8-20230322",
89928999
rendererPackageName: "react-test-renderer"
89939000
};
8994-
var internals$jscomp$inline_1247 = {
8995-
bundleType: devToolsConfig$jscomp$inline_1061.bundleType,
8996-
version: devToolsConfig$jscomp$inline_1061.version,
8997-
rendererPackageName: devToolsConfig$jscomp$inline_1061.rendererPackageName,
8998-
rendererConfig: devToolsConfig$jscomp$inline_1061.rendererConfig,
9001+
var internals$jscomp$inline_1258 = {
9002+
bundleType: devToolsConfig$jscomp$inline_1072.bundleType,
9003+
version: devToolsConfig$jscomp$inline_1072.version,
9004+
rendererPackageName: devToolsConfig$jscomp$inline_1072.rendererPackageName,
9005+
rendererConfig: devToolsConfig$jscomp$inline_1072.rendererConfig,
89999006
overrideHookState: null,
90009007
overrideHookStateDeletePath: null,
90019008
overrideHookStateRenamePath: null,
@@ -9012,26 +9019,26 @@ var internals$jscomp$inline_1247 = {
90129019
return null === fiber ? null : fiber.stateNode;
90139020
},
90149021
findFiberByHostInstance:
9015-
devToolsConfig$jscomp$inline_1061.findFiberByHostInstance ||
9022+
devToolsConfig$jscomp$inline_1072.findFiberByHostInstance ||
90169023
emptyFindFiberByHostInstance,
90179024
findHostInstancesForRefresh: null,
90189025
scheduleRefresh: null,
90199026
scheduleRoot: null,
90209027
setRefreshHandler: null,
90219028
getCurrentFiber: null,
9022-
reconcilerVersion: "18.3.0-next-afb3d51dc-20230322"
9029+
reconcilerVersion: "18.3.0-next-51a7c45f8-20230322"
90239030
};
90249031
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
9025-
var hook$jscomp$inline_1248 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
9032+
var hook$jscomp$inline_1259 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
90269033
if (
9027-
!hook$jscomp$inline_1248.isDisabled &&
9028-
hook$jscomp$inline_1248.supportsFiber
9034+
!hook$jscomp$inline_1259.isDisabled &&
9035+
hook$jscomp$inline_1259.supportsFiber
90299036
)
90309037
try {
9031-
(rendererID = hook$jscomp$inline_1248.inject(
9032-
internals$jscomp$inline_1247
9038+
(rendererID = hook$jscomp$inline_1259.inject(
9039+
internals$jscomp$inline_1258
90339040
)),
9034-
(injectedHook = hook$jscomp$inline_1248);
9041+
(injectedHook = hook$jscomp$inline_1259);
90359042
} catch (err) {}
90369043
}
90379044
exports._Scheduler = Scheduler;

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/cjs/React-dev.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-next-afb3d51dc-20230322";
30+
var ReactVersion = "18.3.0-next-51a7c45f8-20230322";
3131

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

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -639,4 +639,4 @@ exports.useSyncExternalStore = function (
639639
);
640640
};
641641
exports.useTransition = useTransition;
642-
exports.version = "18.3.0-next-afb3d51dc-20230322";
642+
exports.version = "18.3.0-next-51a7c45f8-20230322";

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ exports.useSyncExternalStore = function (
642642
);
643643
};
644644
exports.useTransition = useTransition;
645-
exports.version = "18.3.0-next-afb3d51dc-20230322";
645+
exports.version = "18.3.0-next-51a7c45f8-20230322";
646646

647647
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
648648
if (
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
afb3d51dc6310f0dbeffdd303eb3c6895e6f7db0
1+
51a7c45f8799cab903693fcfdd305ce84ba15273

0 commit comments

Comments
 (0)