Skip to content

Commit f748f73

Browse files
committed
Fix: useDeferredValue initialValue suspends forever without switching to final (#27888)
Fixes a bug in the experimental `initialValue` option for `useDeferredValue` (added in #27500). If rendering the `initialValue` causes the tree to suspend, React should skip it and switch to rendering the final value instead. It should not wait for `initialValue` to resolve. This is not just an optimization, because in some cases the initial value may _never_ resolve — intentionally. For example, if the application does not provide an instant fallback state. This capability is, in fact, the primary motivation for the `initialValue` API. I mostly implemented this correctly in the original PR, but I missed some cases where it wasn't working: - If there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're not in the shell of the transition (i.e. there's a parent Suspense boundary wrapping the `useDeferredValue` hook), the deferred task would get incorrectly dropped. - Similarly, if there's no Suspense boundary between the `useDeferredValue` hook and the component that suspends, and we're rendering a synchronous update, the deferred task would get incorrectly dropped. What these cases have in common is that it causes the `useDeferredValue` hook itself to be replaced by a Suspense fallback. The fix was the same for both. (It already worked in cases where there's no Suspense fallback at all, because those are handled differently, at the root.) The way I discovered this was when investigating a particular bug in Next.js that would happen during a 'popstate' transition (back/forward), but not during a regular navigation. That's because we render popstate transitions synchronously to preserve browser's scroll position — which in this case triggered the second scenario above. DiffTrain build for [f1039be](f1039be)
1 parent f2094ee commit f748f73

19 files changed

+1891
-1133
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c5b9375767e2c4102d7e5559d383523736f1c902
1+
f1039be4a48384e7e4b0a87d4d92c48e900053b9

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ if (__DEV__) {
2424
) {
2525
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2626
}
27-
var ReactVersion = "18.3.0-www-classic-fc109ac4";
27+
var ReactVersion = "18.3.0-www-classic-b43bd001";
2828

2929
// ATTENTION
3030
// When adding new symbols to this file,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,4 +587,4 @@ exports.useSyncExternalStore = function (
587587
exports.useTransition = function () {
588588
return ReactCurrentDispatcher.current.useTransition();
589589
};
590-
exports.version = "18.3.0-www-classic-dd7a0299";
590+
exports.version = "18.3.0-www-classic-9e2eb6c9";

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

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ if (__DEV__) {
6666
return self;
6767
}
6868

69-
var ReactVersion = "18.3.0-www-classic-cb4986de";
69+
var ReactVersion = "18.3.0-www-classic-1401d50e";
7070

7171
var LegacyRoot = 0;
7272
var ConcurrentRoot = 1;
@@ -544,6 +544,7 @@ if (__DEV__) {
544544

545545
var ScheduleRetry = StoreConsistency;
546546
var ShouldSuspendCommit = Visibility;
547+
var DidDefer = ContentReset;
547548
var LifecycleEffectMask =
548549
Passive$1 | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit)
549550

@@ -15591,9 +15592,26 @@ if (__DEV__) {
1559115592
return hasSuspenseListContext(suspenseContext, ForceSuspenseFallback);
1559215593
}
1559315594

15594-
function getRemainingWorkInPrimaryTree(current, renderLanes) {
15595-
// TODO: Should not remove render lanes that were pinged during this render
15596-
return removeLanes(current.childLanes, renderLanes);
15595+
function getRemainingWorkInPrimaryTree(
15596+
current,
15597+
primaryTreeDidDefer,
15598+
renderLanes
15599+
) {
15600+
var remainingLanes =
15601+
current !== null
15602+
? removeLanes(current.childLanes, renderLanes)
15603+
: NoLanes;
15604+
15605+
if (primaryTreeDidDefer) {
15606+
// A useDeferredValue hook spawned a deferred task inside the primary tree.
15607+
// Ensure that we retry this component at the deferred priority.
15608+
// TODO: We could make this a per-subtree value instead of a global one.
15609+
// Would need to track it on the context stack somehow, similar to what
15610+
// we'd have to do for resumable contexts.
15611+
remainingLanes = mergeLanes(remainingLanes, peekDeferredLane());
15612+
}
15613+
15614+
return remainingLanes;
1559715615
}
1559815616

1559915617
function updateSuspenseComponent(current, workInProgress, renderLanes) {
@@ -15613,7 +15631,12 @@ if (__DEV__) {
1561315631
// rendering the fallback children.
1561415632
showFallback = true;
1561515633
workInProgress.flags &= ~DidCapture;
15616-
} // OK, the next part is confusing. We're about to reconcile the Suspense
15634+
} // Check if the primary children spawned a deferred task (useDeferredValue)
15635+
// during the first pass.
15636+
15637+
var didPrimaryChildrenDefer =
15638+
(workInProgress.flags & DidDefer) !== NoFlags$1;
15639+
workInProgress.flags &= ~DidDefer; // OK, the next part is confusing. We're about to reconcile the Suspense
1561715640
// boundary's children. This involves some custom reconciliation logic. Two
1561815641
// main reasons this is so complicated.
1561915642
//
@@ -15651,6 +15674,11 @@ if (__DEV__) {
1565115674
var primaryChildFragment = workInProgress.child;
1565215675
primaryChildFragment.memoizedState =
1565315676
mountSuspenseOffscreenState(renderLanes);
15677+
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
15678+
current,
15679+
didPrimaryChildrenDefer,
15680+
renderLanes
15681+
);
1565415682
workInProgress.memoizedState = SUSPENDED_MARKER;
1565515683

1565615684
if (enableTransitionTracing) {
@@ -15691,6 +15719,11 @@ if (__DEV__) {
1569115719
var _primaryChildFragment = workInProgress.child;
1569215720
_primaryChildFragment.memoizedState =
1569315721
mountSuspenseOffscreenState(renderLanes);
15722+
_primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
15723+
current,
15724+
didPrimaryChildrenDefer,
15725+
renderLanes
15726+
);
1569415727
workInProgress.memoizedState = SUSPENDED_MARKER; // TODO: Transition Tracing is not yet implemented for CPU Suspense.
1569515728
// Since nothing actually suspended, there will nothing to ping this to
1569615729
// get it started back up to attempt the next item. While in terms of
@@ -15723,6 +15756,7 @@ if (__DEV__) {
1572315756
current,
1572415757
workInProgress,
1572515758
didSuspend,
15759+
didPrimaryChildrenDefer,
1572615760
nextProps,
1572715761
_dehydrated,
1572815762
prevState,
@@ -15786,6 +15820,7 @@ if (__DEV__) {
1578615820

1578715821
_primaryChildFragment2.childLanes = getRemainingWorkInPrimaryTree(
1578815822
current,
15823+
didPrimaryChildrenDefer,
1578915824
renderLanes
1579015825
);
1579115826
workInProgress.memoizedState = SUSPENDED_MARKER;
@@ -16104,6 +16139,7 @@ if (__DEV__) {
1610416139
current,
1610516140
workInProgress,
1610616141
didSuspend,
16142+
didPrimaryChildrenDefer,
1610716143
nextProps,
1610816144
suspenseInstance,
1610916145
suspenseState,
@@ -16320,6 +16356,11 @@ if (__DEV__) {
1632016356
var _primaryChildFragment4 = workInProgress.child;
1632116357
_primaryChildFragment4.memoizedState =
1632216358
mountSuspenseOffscreenState(renderLanes);
16359+
_primaryChildFragment4.childLanes = getRemainingWorkInPrimaryTree(
16360+
current,
16361+
didPrimaryChildrenDefer,
16362+
renderLanes
16363+
);
1632316364
workInProgress.memoizedState = SUSPENDED_MARKER;
1632416365
return fallbackChildFragment;
1632516366
}
@@ -24766,10 +24807,22 @@ if (__DEV__) {
2476624807
// Everything else is spawned as a transition.
2476724808
workInProgressDeferredLane = requestTransitionLane();
2476824809
}
24810+
} // Mark the parent Suspense boundary so it knows to spawn the deferred lane.
24811+
24812+
var suspenseHandler = getSuspenseHandler();
24813+
24814+
if (suspenseHandler !== null) {
24815+
// TODO: As an optimization, we shouldn't entangle the lanes at the root; we
24816+
// can entangle them using the baseLanes of the Suspense boundary instead.
24817+
// We only need to do something special if there's no Suspense boundary.
24818+
suspenseHandler.flags |= DidDefer;
2476924819
}
2477024820

2477124821
return workInProgressDeferredLane;
2477224822
}
24823+
function peekDeferredLane() {
24824+
return workInProgressDeferredLane;
24825+
}
2477324826
function scheduleUpdateOnFiber(root, fiber, lane) {
2477424827
{
2477524828
if (isRunningInsertionEffect) {
@@ -25387,7 +25440,7 @@ if (__DEV__) {
2538725440
// The render unwound without completing the tree. This happens in special
2538825441
// cases where need to exit the current render without producing a
2538925442
// consistent tree or committing.
25390-
markRootSuspended(root, lanes, NoLane);
25443+
markRootSuspended(root, lanes, workInProgressDeferredLane);
2539125444
ensureRootIsScheduled(root);
2539225445
return null;
2539325446
} // We now have a consistent tree. Because this is a sync render, we

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

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ if (__DEV__) {
6666
return self;
6767
}
6868

69-
var ReactVersion = "18.3.0-www-modern-0a58ac99";
69+
var ReactVersion = "18.3.0-www-modern-a32ad479";
7070

7171
var LegacyRoot = 0;
7272
var ConcurrentRoot = 1;
@@ -544,6 +544,7 @@ if (__DEV__) {
544544

545545
var ScheduleRetry = StoreConsistency;
546546
var ShouldSuspendCommit = Visibility;
547+
var DidDefer = ContentReset;
547548
var LifecycleEffectMask =
548549
Passive$1 | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit)
549550

@@ -15270,9 +15271,26 @@ if (__DEV__) {
1527015271
return hasSuspenseListContext(suspenseContext, ForceSuspenseFallback);
1527115272
}
1527215273

15273-
function getRemainingWorkInPrimaryTree(current, renderLanes) {
15274-
// TODO: Should not remove render lanes that were pinged during this render
15275-
return removeLanes(current.childLanes, renderLanes);
15274+
function getRemainingWorkInPrimaryTree(
15275+
current,
15276+
primaryTreeDidDefer,
15277+
renderLanes
15278+
) {
15279+
var remainingLanes =
15280+
current !== null
15281+
? removeLanes(current.childLanes, renderLanes)
15282+
: NoLanes;
15283+
15284+
if (primaryTreeDidDefer) {
15285+
// A useDeferredValue hook spawned a deferred task inside the primary tree.
15286+
// Ensure that we retry this component at the deferred priority.
15287+
// TODO: We could make this a per-subtree value instead of a global one.
15288+
// Would need to track it on the context stack somehow, similar to what
15289+
// we'd have to do for resumable contexts.
15290+
remainingLanes = mergeLanes(remainingLanes, peekDeferredLane());
15291+
}
15292+
15293+
return remainingLanes;
1527615294
}
1527715295

1527815296
function updateSuspenseComponent(current, workInProgress, renderLanes) {
@@ -15292,7 +15310,12 @@ if (__DEV__) {
1529215310
// rendering the fallback children.
1529315311
showFallback = true;
1529415312
workInProgress.flags &= ~DidCapture;
15295-
} // OK, the next part is confusing. We're about to reconcile the Suspense
15313+
} // Check if the primary children spawned a deferred task (useDeferredValue)
15314+
// during the first pass.
15315+
15316+
var didPrimaryChildrenDefer =
15317+
(workInProgress.flags & DidDefer) !== NoFlags$1;
15318+
workInProgress.flags &= ~DidDefer; // OK, the next part is confusing. We're about to reconcile the Suspense
1529615319
// boundary's children. This involves some custom reconciliation logic. Two
1529715320
// main reasons this is so complicated.
1529815321
//
@@ -15330,6 +15353,11 @@ if (__DEV__) {
1533015353
var primaryChildFragment = workInProgress.child;
1533115354
primaryChildFragment.memoizedState =
1533215355
mountSuspenseOffscreenState(renderLanes);
15356+
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
15357+
current,
15358+
didPrimaryChildrenDefer,
15359+
renderLanes
15360+
);
1533315361
workInProgress.memoizedState = SUSPENDED_MARKER;
1533415362

1533515363
if (enableTransitionTracing) {
@@ -15370,6 +15398,11 @@ if (__DEV__) {
1537015398
var _primaryChildFragment = workInProgress.child;
1537115399
_primaryChildFragment.memoizedState =
1537215400
mountSuspenseOffscreenState(renderLanes);
15401+
_primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
15402+
current,
15403+
didPrimaryChildrenDefer,
15404+
renderLanes
15405+
);
1537315406
workInProgress.memoizedState = SUSPENDED_MARKER; // TODO: Transition Tracing is not yet implemented for CPU Suspense.
1537415407
// Since nothing actually suspended, there will nothing to ping this to
1537515408
// get it started back up to attempt the next item. While in terms of
@@ -15402,6 +15435,7 @@ if (__DEV__) {
1540215435
current,
1540315436
workInProgress,
1540415437
didSuspend,
15438+
didPrimaryChildrenDefer,
1540515439
nextProps,
1540615440
_dehydrated,
1540715441
prevState,
@@ -15465,6 +15499,7 @@ if (__DEV__) {
1546515499

1546615500
_primaryChildFragment2.childLanes = getRemainingWorkInPrimaryTree(
1546715501
current,
15502+
didPrimaryChildrenDefer,
1546815503
renderLanes
1546915504
);
1547015505
workInProgress.memoizedState = SUSPENDED_MARKER;
@@ -15783,6 +15818,7 @@ if (__DEV__) {
1578315818
current,
1578415819
workInProgress,
1578515820
didSuspend,
15821+
didPrimaryChildrenDefer,
1578615822
nextProps,
1578715823
suspenseInstance,
1578815824
suspenseState,
@@ -15999,6 +16035,11 @@ if (__DEV__) {
1599916035
var _primaryChildFragment4 = workInProgress.child;
1600016036
_primaryChildFragment4.memoizedState =
1600116037
mountSuspenseOffscreenState(renderLanes);
16038+
_primaryChildFragment4.childLanes = getRemainingWorkInPrimaryTree(
16039+
current,
16040+
didPrimaryChildrenDefer,
16041+
renderLanes
16042+
);
1600216043
workInProgress.memoizedState = SUSPENDED_MARKER;
1600316044
return fallbackChildFragment;
1600416045
}
@@ -24410,10 +24451,22 @@ if (__DEV__) {
2441024451
// Everything else is spawned as a transition.
2441124452
workInProgressDeferredLane = requestTransitionLane();
2441224453
}
24454+
} // Mark the parent Suspense boundary so it knows to spawn the deferred lane.
24455+
24456+
var suspenseHandler = getSuspenseHandler();
24457+
24458+
if (suspenseHandler !== null) {
24459+
// TODO: As an optimization, we shouldn't entangle the lanes at the root; we
24460+
// can entangle them using the baseLanes of the Suspense boundary instead.
24461+
// We only need to do something special if there's no Suspense boundary.
24462+
suspenseHandler.flags |= DidDefer;
2441324463
}
2441424464

2441524465
return workInProgressDeferredLane;
2441624466
}
24467+
function peekDeferredLane() {
24468+
return workInProgressDeferredLane;
24469+
}
2441724470
function scheduleUpdateOnFiber(root, fiber, lane) {
2441824471
{
2441924472
if (isRunningInsertionEffect) {
@@ -25031,7 +25084,7 @@ if (__DEV__) {
2503125084
// The render unwound without completing the tree. This happens in special
2503225085
// cases where need to exit the current render without producing a
2503325086
// consistent tree or committing.
25034-
markRootSuspended(root, lanes, NoLane);
25087+
markRootSuspended(root, lanes, workInProgressDeferredLane);
2503525088
ensureRootIsScheduled(root);
2503625089
return null;
2503725090
} // We now have a consistent tree. Because this is a sync render, we

0 commit comments

Comments
 (0)