Skip to content

Commit 92eda60

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 commit f1039be.
1 parent 89da9fc commit 92eda60

File tree

13 files changed

+908
-524
lines changed

13 files changed

+908
-524
lines changed

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<1664f9f3eec9720b0472e436e7889c65>>
10+
* @generated SignedSource<<b85e6b4f686f35ddcd77f85e17cfc9f9>>
1111
*/
1212

1313
"use strict";
@@ -488,6 +488,7 @@ if (__DEV__) {
488488

489489
var ScheduleRetry = StoreConsistency;
490490
var ShouldSuspendCommit = Visibility;
491+
var DidDefer = ContentReset;
491492
var LifecycleEffectMask =
492493
Passive$1 | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit)
493494

@@ -13945,9 +13946,26 @@ if (__DEV__) {
1394513946
return hasSuspenseListContext(suspenseContext, ForceSuspenseFallback);
1394613947
}
1394713948

13948-
function getRemainingWorkInPrimaryTree(current, renderLanes) {
13949-
// TODO: Should not remove render lanes that were pinged during this render
13950-
return removeLanes(current.childLanes, renderLanes);
13949+
function getRemainingWorkInPrimaryTree(
13950+
current,
13951+
primaryTreeDidDefer,
13952+
renderLanes
13953+
) {
13954+
var remainingLanes =
13955+
current !== null
13956+
? removeLanes(current.childLanes, renderLanes)
13957+
: NoLanes;
13958+
13959+
if (primaryTreeDidDefer) {
13960+
// A useDeferredValue hook spawned a deferred task inside the primary tree.
13961+
// Ensure that we retry this component at the deferred priority.
13962+
// TODO: We could make this a per-subtree value instead of a global one.
13963+
// Would need to track it on the context stack somehow, similar to what
13964+
// we'd have to do for resumable contexts.
13965+
remainingLanes = mergeLanes(remainingLanes, peekDeferredLane());
13966+
}
13967+
13968+
return remainingLanes;
1395113969
}
1395213970

1395313971
function updateSuspenseComponent(current, workInProgress, renderLanes) {
@@ -13967,7 +13985,12 @@ if (__DEV__) {
1396713985
// rendering the fallback children.
1396813986
showFallback = true;
1396913987
workInProgress.flags &= ~DidCapture;
13970-
} // OK, the next part is confusing. We're about to reconcile the Suspense
13988+
} // Check if the primary children spawned a deferred task (useDeferredValue)
13989+
// during the first pass.
13990+
13991+
var didPrimaryChildrenDefer =
13992+
(workInProgress.flags & DidDefer) !== NoFlags$1;
13993+
workInProgress.flags &= ~DidDefer; // OK, the next part is confusing. We're about to reconcile the Suspense
1397113994
// boundary's children. This involves some custom reconciliation logic. Two
1397213995
// main reasons this is so complicated.
1397313996
//
@@ -14005,6 +14028,11 @@ if (__DEV__) {
1400514028
var primaryChildFragment = workInProgress.child;
1400614029
primaryChildFragment.memoizedState =
1400714030
mountSuspenseOffscreenState(renderLanes);
14031+
primaryChildFragment.childLanes = getRemainingWorkInPrimaryTree(
14032+
current,
14033+
didPrimaryChildrenDefer,
14034+
renderLanes
14035+
);
1400814036
workInProgress.memoizedState = SUSPENDED_MARKER;
1400914037

1401014038
return fallbackFragment;
@@ -14028,6 +14056,7 @@ if (__DEV__) {
1402814056
current,
1402914057
workInProgress,
1403014058
didSuspend,
14059+
didPrimaryChildrenDefer,
1403114060
nextProps,
1403214061
_dehydrated,
1403314062
prevState,
@@ -14056,6 +14085,7 @@ if (__DEV__) {
1405614085

1405714086
_primaryChildFragment2.childLanes = getRemainingWorkInPrimaryTree(
1405814087
current,
14088+
didPrimaryChildrenDefer,
1405914089
renderLanes
1406014090
);
1406114091
workInProgress.memoizedState = SUSPENDED_MARKER;
@@ -14374,6 +14404,7 @@ if (__DEV__) {
1437414404
current,
1437514405
workInProgress,
1437614406
didSuspend,
14407+
didPrimaryChildrenDefer,
1437714408
nextProps,
1437814409
suspenseInstance,
1437914410
suspenseState,
@@ -14575,6 +14606,11 @@ if (__DEV__) {
1457514606
var _primaryChildFragment4 = workInProgress.child;
1457614607
_primaryChildFragment4.memoizedState =
1457714608
mountSuspenseOffscreenState(renderLanes);
14609+
_primaryChildFragment4.childLanes = getRemainingWorkInPrimaryTree(
14610+
current,
14611+
didPrimaryChildrenDefer,
14612+
renderLanes
14613+
);
1457814614
workInProgress.memoizedState = SUSPENDED_MARKER;
1457914615
return fallbackChildFragment;
1458014616
}
@@ -21570,10 +21606,22 @@ if (__DEV__) {
2157021606
// Everything else is spawned as a transition.
2157121607
workInProgressDeferredLane = requestTransitionLane();
2157221608
}
21609+
} // Mark the parent Suspense boundary so it knows to spawn the deferred lane.
21610+
21611+
var suspenseHandler = getSuspenseHandler();
21612+
21613+
if (suspenseHandler !== null) {
21614+
// TODO: As an optimization, we shouldn't entangle the lanes at the root; we
21615+
// can entangle them using the baseLanes of the Suspense boundary instead.
21616+
// We only need to do something special if there's no Suspense boundary.
21617+
suspenseHandler.flags |= DidDefer;
2157321618
}
2157421619

2157521620
return workInProgressDeferredLane;
2157621621
}
21622+
function peekDeferredLane() {
21623+
return workInProgressDeferredLane;
21624+
}
2157721625
function scheduleUpdateOnFiber(root, fiber, lane) {
2157821626
{
2157921627
if (isRunningInsertionEffect) {
@@ -22142,7 +22190,7 @@ if (__DEV__) {
2214222190
// The render unwound without completing the tree. This happens in special
2214322191
// cases where need to exit the current render without producing a
2214422192
// consistent tree or committing.
22145-
markRootSuspended(root, lanes, NoLane);
22193+
markRootSuspended(root, lanes, workInProgressDeferredLane);
2214622194
ensureRootIsScheduled(root);
2214722195
return null;
2214822196
} // We now have a consistent tree. Because this is a sync render, we
@@ -25476,7 +25524,7 @@ if (__DEV__) {
2547625524
return root;
2547725525
}
2547825526

25479-
var ReactVersion = "18.3.0-canary-1d5667a12-20240102";
25527+
var ReactVersion = "18.3.0-canary-f1039be4a-20240107";
2548025528

2548125529
// Might add PROFILE later.
2548225530

0 commit comments

Comments
 (0)