Skip to content

Commit 4ca2342

Browse files
committed
Fix: useTransition after use gets stuck in pending state (#29670)
When a component suspends with `use`, we switch to the "re-render" dispatcher during the subsequent render attempt, so that we can reuse the work from the initial attempt. However, once we run out of hooks from the previous attempt, we should switch back to the regular "update" dispatcher. This is conceptually the same fix as the one introduced in #26232. That fix only accounted for initial mount, but the useTransition regression test added in f829733 illustrates that we need to handle updates, too. The issue affects more than just useTransition but because most of the behavior between the "re-render" and "update" dispatchers is the same it's hard to contrive other scenarios in a test, which is probably why it took so long for someone to notice. Closes #28923 and #29209 --------- Co-authored-by: eps1lon <sebastian.silbermann@vercel.com> DiffTrain build for [adbec0c](adbec0c)
1 parent 11f5503 commit 4ca2342

32 files changed

+458
-168
lines changed

compiled/facebook-www/REVISION

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c69211a9dfa683038b1a758aba2ca09c7862a6d3
1+
adbec0c25aff07f04b0678679554505ba2813168
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c69211a9dfa683038b1a758aba2ca09c7862a6d3
1+
adbec0c25aff07f04b0678679554505ba2813168

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ if (
2222
) {
2323
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2424
}
25-
var ReactVersion = '19.0.0-www-classic-c69211a9df-20240531';
25+
var ReactVersion = '19.0.0-www-classic-adbec0c25a-20240531';
2626

2727
// Re-export dynamic flags from the www version.
2828
var dynamicFeatureFlags = require('ReactFeatureFlags');

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ if (
2222
) {
2323
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2424
}
25-
var ReactVersion = '19.0.0-www-modern-c69211a9df-20240531';
25+
var ReactVersion = '19.0.0-www-modern-adbec0c25a-20240531';
2626

2727
// Re-export dynamic flags from the www version.
2828
var dynamicFeatureFlags = require('ReactFeatureFlags');

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -684,4 +684,4 @@ exports.useSyncExternalStore = function (
684684
exports.useTransition = function () {
685685
return ReactSharedInternals.H.useTransition();
686686
};
687-
exports.version = "19.0.0-www-classic-c69211a9df-20240531";
687+
exports.version = "19.0.0-www-classic-adbec0c25a-20240531";

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -684,4 +684,4 @@ exports.useSyncExternalStore = function (
684684
exports.useTransition = function () {
685685
return ReactSharedInternals.H.useTransition();
686686
};
687-
exports.version = "19.0.0-www-modern-c69211a9df-20240531";
687+
exports.version = "19.0.0-www-modern-adbec0c25a-20240531";

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ exports.useSyncExternalStore = function (
688688
exports.useTransition = function () {
689689
return ReactSharedInternals.H.useTransition();
690690
};
691-
exports.version = "19.0.0-www-classic-c69211a9df-20240531";
691+
exports.version = "19.0.0-www-classic-adbec0c25a-20240531";
692692
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
693693
"function" ===
694694
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ exports.useSyncExternalStore = function (
688688
exports.useTransition = function () {
689689
return ReactSharedInternals.H.useTransition();
690690
};
691-
exports.version = "19.0.0-www-modern-c69211a9df-20240531";
691+
exports.version = "19.0.0-www-modern-adbec0c25a-20240531";
692692
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
693693
"function" ===
694694
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

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

+32-8
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function _assertThisInitialized(self) {
6060
return self;
6161
}
6262

63-
var ReactVersion = '19.0.0-www-classic-c69211a9df-20240531';
63+
var ReactVersion = '19.0.0-www-classic-adbec0c25a-20240531';
6464

6565
var LegacyRoot = 0;
6666
var ConcurrentRoot = 1;
@@ -8503,15 +8503,39 @@ function useThenable(thenable) {
85038503
thenableState = createThenableState();
85048504
}
85058505

8506-
var result = trackUsedThenable(thenableState, thenable, index);
8506+
var result = trackUsedThenable(thenableState, thenable, index); // When something suspends with `use`, we replay the component with the
8507+
// "re-render" dispatcher instead of the "mount" or "update" dispatcher.
8508+
//
8509+
// But if there are additional hooks that occur after the `use` invocation
8510+
// that suspended, they wouldn't have been processed during the previous
8511+
// attempt. So after we invoke `use` again, we may need to switch from the
8512+
// "re-render" dispatcher back to the "mount" or "update" dispatcher. That's
8513+
// what the following logic accounts for.
8514+
//
8515+
// TODO: Theoretically this logic only needs to go into the rerender
8516+
// dispatcher. Could optimize, but probably not be worth it.
8517+
// This is the same logic as in updateWorkInProgressHook.
8518+
8519+
var workInProgressFiber = currentlyRenderingFiber$1;
8520+
var nextWorkInProgressHook = workInProgressHook === null ? // We're at the beginning of the list, so read from the first hook from
8521+
// the fiber.
8522+
workInProgressFiber.memoizedState : workInProgressHook.next;
8523+
8524+
if (nextWorkInProgressHook !== null) ; else {
8525+
// There are no remaining hooks from the previous attempt. We're no longer
8526+
// in "re-render" mode. Switch to the normal mount or update dispatcher.
8527+
//
8528+
// This is the same as the logic in renderWithHooks, except we don't bother
8529+
// to track the hook types debug information in this case (sufficient to
8530+
// only do that when nothing suspends).
8531+
var currentFiber = workInProgressFiber.alternate;
85078532

8508-
if (currentlyRenderingFiber$1.alternate === null && (workInProgressHook === null ? currentlyRenderingFiber$1.memoizedState === null : workInProgressHook.next === null)) {
8509-
// Initial render, and either this is the first time the component is
8510-
// called, or there were no Hooks called after this use() the previous
8511-
// time (perhaps because it threw). Subsequent Hook calls should use the
8512-
// mount dispatcher.
85138533
{
8514-
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
8534+
if (currentFiber !== null && currentFiber.memoizedState !== null) {
8535+
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
8536+
} else {
8537+
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
8538+
}
85158539
}
85168540
}
85178541

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

+32-8
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function _assertThisInitialized(self) {
6060
return self;
6161
}
6262

63-
var ReactVersion = '19.0.0-www-modern-c69211a9df-20240531';
63+
var ReactVersion = '19.0.0-www-modern-adbec0c25a-20240531';
6464

6565
var LegacyRoot = 0;
6666
var ConcurrentRoot = 1;
@@ -8292,15 +8292,39 @@ function useThenable(thenable) {
82928292
thenableState = createThenableState();
82938293
}
82948294

8295-
var result = trackUsedThenable(thenableState, thenable, index);
8295+
var result = trackUsedThenable(thenableState, thenable, index); // When something suspends with `use`, we replay the component with the
8296+
// "re-render" dispatcher instead of the "mount" or "update" dispatcher.
8297+
//
8298+
// But if there are additional hooks that occur after the `use` invocation
8299+
// that suspended, they wouldn't have been processed during the previous
8300+
// attempt. So after we invoke `use` again, we may need to switch from the
8301+
// "re-render" dispatcher back to the "mount" or "update" dispatcher. That's
8302+
// what the following logic accounts for.
8303+
//
8304+
// TODO: Theoretically this logic only needs to go into the rerender
8305+
// dispatcher. Could optimize, but probably not be worth it.
8306+
// This is the same logic as in updateWorkInProgressHook.
8307+
8308+
var workInProgressFiber = currentlyRenderingFiber$1;
8309+
var nextWorkInProgressHook = workInProgressHook === null ? // We're at the beginning of the list, so read from the first hook from
8310+
// the fiber.
8311+
workInProgressFiber.memoizedState : workInProgressHook.next;
8312+
8313+
if (nextWorkInProgressHook !== null) ; else {
8314+
// There are no remaining hooks from the previous attempt. We're no longer
8315+
// in "re-render" mode. Switch to the normal mount or update dispatcher.
8316+
//
8317+
// This is the same as the logic in renderWithHooks, except we don't bother
8318+
// to track the hook types debug information in this case (sufficient to
8319+
// only do that when nothing suspends).
8320+
var currentFiber = workInProgressFiber.alternate;
82968321

8297-
if (currentlyRenderingFiber$1.alternate === null && (workInProgressHook === null ? currentlyRenderingFiber$1.memoizedState === null : workInProgressHook.next === null)) {
8298-
// Initial render, and either this is the first time the component is
8299-
// called, or there were no Hooks called after this use() the previous
8300-
// time (perhaps because it threw). Subsequent Hook calls should use the
8301-
// mount dispatcher.
83028322
{
8303-
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
8323+
if (currentFiber !== null && currentFiber.memoizedState !== null) {
8324+
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
8325+
} else {
8326+
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
8327+
}
83048328
}
83058329
}
83068330

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -2724,11 +2724,16 @@ function useThenable(thenable) {
27242724
thenableIndexCounter += 1;
27252725
null === thenableState && (thenableState = []);
27262726
thenable = trackUsedThenable(thenableState, thenable, index);
2727-
null === currentlyRenderingFiber$1.alternate &&
2727+
index = currentlyRenderingFiber$1;
2728+
null ===
27282729
(null === workInProgressHook
2729-
? null === currentlyRenderingFiber$1.memoizedState
2730-
: null === workInProgressHook.next) &&
2731-
(ReactSharedInternals.H = HooksDispatcherOnMount);
2730+
? index.memoizedState
2731+
: workInProgressHook.next) &&
2732+
((index = index.alternate),
2733+
(ReactSharedInternals.H =
2734+
null === index || null === index.memoizedState
2735+
? HooksDispatcherOnMount
2736+
: HooksDispatcherOnUpdate));
27322737
return thenable;
27332738
}
27342739
function use(usable) {
@@ -10640,7 +10645,7 @@ var slice = Array.prototype.slice,
1064010645
return null;
1064110646
},
1064210647
bundleType: 0,
10643-
version: "19.0.0-www-classic-c69211a9df-20240531",
10648+
version: "19.0.0-www-classic-adbec0c25a-20240531",
1064410649
rendererPackageName: "react-art"
1064510650
};
1064610651
var internals$jscomp$inline_1370 = {
@@ -10671,7 +10676,7 @@ var internals$jscomp$inline_1370 = {
1067110676
scheduleRoot: null,
1067210677
setRefreshHandler: null,
1067310678
getCurrentFiber: null,
10674-
reconcilerVersion: "19.0.0-www-classic-c69211a9df-20240531"
10679+
reconcilerVersion: "19.0.0-www-classic-adbec0c25a-20240531"
1067510680
};
1067610681
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
1067710682
var hook$jscomp$inline_1371 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

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

+11-6
Original file line numberDiff line numberDiff line change
@@ -2522,11 +2522,16 @@ function useThenable(thenable) {
25222522
thenableIndexCounter += 1;
25232523
null === thenableState && (thenableState = []);
25242524
thenable = trackUsedThenable(thenableState, thenable, index);
2525-
null === currentlyRenderingFiber$1.alternate &&
2525+
index = currentlyRenderingFiber$1;
2526+
null ===
25262527
(null === workInProgressHook
2527-
? null === currentlyRenderingFiber$1.memoizedState
2528-
: null === workInProgressHook.next) &&
2529-
(ReactSharedInternals.H = HooksDispatcherOnMount);
2528+
? index.memoizedState
2529+
: workInProgressHook.next) &&
2530+
((index = index.alternate),
2531+
(ReactSharedInternals.H =
2532+
null === index || null === index.memoizedState
2533+
? HooksDispatcherOnMount
2534+
: HooksDispatcherOnUpdate));
25302535
return thenable;
25312536
}
25322537
function use(usable) {
@@ -10115,7 +10120,7 @@ var slice = Array.prototype.slice,
1011510120
return null;
1011610121
},
1011710122
bundleType: 0,
10118-
version: "19.0.0-www-modern-c69211a9df-20240531",
10123+
version: "19.0.0-www-modern-adbec0c25a-20240531",
1011910124
rendererPackageName: "react-art"
1012010125
};
1012110126
var internals$jscomp$inline_1356 = {
@@ -10146,7 +10151,7 @@ var internals$jscomp$inline_1356 = {
1014610151
scheduleRoot: null,
1014710152
setRefreshHandler: null,
1014810153
getCurrentFiber: null,
10149-
reconcilerVersion: "19.0.0-www-modern-c69211a9df-20240531"
10154+
reconcilerVersion: "19.0.0-www-modern-adbec0c25a-20240531"
1015010155
};
1015110156
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
1015210157
var hook$jscomp$inline_1357 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

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

+32-8
Original file line numberDiff line numberDiff line change
@@ -12178,15 +12178,39 @@ function useThenable(thenable) {
1217812178
thenableState = createThenableState();
1217912179
}
1218012180

12181-
var result = trackUsedThenable(thenableState, thenable, index);
12181+
var result = trackUsedThenable(thenableState, thenable, index); // When something suspends with `use`, we replay the component with the
12182+
// "re-render" dispatcher instead of the "mount" or "update" dispatcher.
12183+
//
12184+
// But if there are additional hooks that occur after the `use` invocation
12185+
// that suspended, they wouldn't have been processed during the previous
12186+
// attempt. So after we invoke `use` again, we may need to switch from the
12187+
// "re-render" dispatcher back to the "mount" or "update" dispatcher. That's
12188+
// what the following logic accounts for.
12189+
//
12190+
// TODO: Theoretically this logic only needs to go into the rerender
12191+
// dispatcher. Could optimize, but probably not be worth it.
12192+
// This is the same logic as in updateWorkInProgressHook.
12193+
12194+
var workInProgressFiber = currentlyRenderingFiber$1;
12195+
var nextWorkInProgressHook = workInProgressHook === null ? // We're at the beginning of the list, so read from the first hook from
12196+
// the fiber.
12197+
workInProgressFiber.memoizedState : workInProgressHook.next;
12198+
12199+
if (nextWorkInProgressHook !== null) ; else {
12200+
// There are no remaining hooks from the previous attempt. We're no longer
12201+
// in "re-render" mode. Switch to the normal mount or update dispatcher.
12202+
//
12203+
// This is the same as the logic in renderWithHooks, except we don't bother
12204+
// to track the hook types debug information in this case (sufficient to
12205+
// only do that when nothing suspends).
12206+
var currentFiber = workInProgressFiber.alternate;
1218212207

12183-
if (currentlyRenderingFiber$1.alternate === null && (workInProgressHook === null ? currentlyRenderingFiber$1.memoizedState === null : workInProgressHook.next === null)) {
12184-
// Initial render, and either this is the first time the component is
12185-
// called, or there were no Hooks called after this use() the previous
12186-
// time (perhaps because it threw). Subsequent Hook calls should use the
12187-
// mount dispatcher.
1218812208
{
12189-
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
12209+
if (currentFiber !== null && currentFiber.memoizedState !== null) {
12210+
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
12211+
} else {
12212+
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
12213+
}
1219012214
}
1219112215
}
1219212216

@@ -31059,7 +31083,7 @@ identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transition
3105931083
return root;
3106031084
}
3106131085

31062-
var ReactVersion = '19.0.0-www-classic-c69211a9df-20240531';
31086+
var ReactVersion = '19.0.0-www-classic-adbec0c25a-20240531';
3106331087

3106431088
function createPortal$1(children, containerInfo, // TODO: figure out the API for cross-renderer implementation.
3106531089
implementation) {

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

+32-8
Original file line numberDiff line numberDiff line change
@@ -11919,15 +11919,39 @@ function useThenable(thenable) {
1191911919
thenableState = createThenableState();
1192011920
}
1192111921

11922-
var result = trackUsedThenable(thenableState, thenable, index);
11922+
var result = trackUsedThenable(thenableState, thenable, index); // When something suspends with `use`, we replay the component with the
11923+
// "re-render" dispatcher instead of the "mount" or "update" dispatcher.
11924+
//
11925+
// But if there are additional hooks that occur after the `use` invocation
11926+
// that suspended, they wouldn't have been processed during the previous
11927+
// attempt. So after we invoke `use` again, we may need to switch from the
11928+
// "re-render" dispatcher back to the "mount" or "update" dispatcher. That's
11929+
// what the following logic accounts for.
11930+
//
11931+
// TODO: Theoretically this logic only needs to go into the rerender
11932+
// dispatcher. Could optimize, but probably not be worth it.
11933+
// This is the same logic as in updateWorkInProgressHook.
11934+
11935+
var workInProgressFiber = currentlyRenderingFiber$1;
11936+
var nextWorkInProgressHook = workInProgressHook === null ? // We're at the beginning of the list, so read from the first hook from
11937+
// the fiber.
11938+
workInProgressFiber.memoizedState : workInProgressHook.next;
11939+
11940+
if (nextWorkInProgressHook !== null) ; else {
11941+
// There are no remaining hooks from the previous attempt. We're no longer
11942+
// in "re-render" mode. Switch to the normal mount or update dispatcher.
11943+
//
11944+
// This is the same as the logic in renderWithHooks, except we don't bother
11945+
// to track the hook types debug information in this case (sufficient to
11946+
// only do that when nothing suspends).
11947+
var currentFiber = workInProgressFiber.alternate;
1192311948

11924-
if (currentlyRenderingFiber$1.alternate === null && (workInProgressHook === null ? currentlyRenderingFiber$1.memoizedState === null : workInProgressHook.next === null)) {
11925-
// Initial render, and either this is the first time the component is
11926-
// called, or there were no Hooks called after this use() the previous
11927-
// time (perhaps because it threw). Subsequent Hook calls should use the
11928-
// mount dispatcher.
1192911949
{
11930-
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
11950+
if (currentFiber !== null && currentFiber.memoizedState !== null) {
11951+
ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;
11952+
} else {
11953+
ReactSharedInternals.H = HooksDispatcherOnMountInDEV;
11954+
}
1193111955
}
1193211956
}
1193311957

@@ -30236,7 +30260,7 @@ identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transition
3023630260
return root;
3023730261
}
3023830262

30239-
var ReactVersion = '19.0.0-www-modern-c69211a9df-20240531';
30263+
var ReactVersion = '19.0.0-www-modern-adbec0c25a-20240531';
3024030264

3024130265
function createPortal$1(children, containerInfo, // TODO: figure out the API for cross-renderer implementation.
3024230266
implementation) {

0 commit comments

Comments
 (0)