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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c69211a9dfa683038b1a758aba2ca09c7862a6d3
1+
adbec0c25aff07f04b0678679554505ba2813168
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c69211a9dfa683038b1a758aba2ca09c7862a6d3
1+
adbec0c25aff07f04b0678679554505ba2813168

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 32 additions & 8 deletions
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

Lines changed: 32 additions & 8 deletions
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

0 commit comments

Comments
 (0)