Skip to content

Commit 63a171b

Browse files
committed
useDeferredValue switches to final if initial suspends
If a parent render spawns a deferred task with useDeferredValue, but the parent render suspends, we should not wait for the parent render to complete before attempting to render the final value. The reason is that the initialValue argument to useDeferredValue is meant to represent an immediate preview of the final UI. If we can't render it "immediately", we might as well skip it and go straight to the "real" value. This is an improvement over how a userspace implementation of useDeferredValue would work, because a userspace implementation would have to wait for the parent task to commit (useEffect) before spawning the deferred task, creating a waterfall.
1 parent 7f1ce0b commit 63a171b

File tree

4 files changed

+394
-27
lines changed

4 files changed

+394
-27
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,17 @@ import {
6161
NoLane,
6262
SyncLane,
6363
OffscreenLane,
64+
DeferredLane,
6465
NoLanes,
6566
isSubsetOfLanes,
6667
includesBlockingLane,
6768
includesOnlyNonUrgentLanes,
68-
claimNextTransitionLane,
6969
mergeLanes,
7070
removeLanes,
7171
intersectLanes,
7272
isTransitionLane,
7373
markRootEntangled,
74+
includesSomeLane,
7475
} from './ReactFiberLane';
7576
import {
7677
ContinuousEventPriority,
@@ -101,6 +102,7 @@ import {
101102
getWorkInProgressRootRenderLanes,
102103
scheduleUpdateOnFiber,
103104
requestUpdateLane,
105+
requestDeferredLane,
104106
markSkippedUpdateLanes,
105107
isInvalidExecutionContextForEventFunction,
106108
} from './ReactFiberWorkLoop';
@@ -2665,16 +2667,21 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
26652667
}
26662668

26672669
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
2668-
if (enableUseDeferredValueInitialArg && initialValue !== undefined) {
2670+
if (
2671+
enableUseDeferredValueInitialArg &&
26692672
// When `initialValue` is provided, we defer the initial render even if the
26702673
// current render is not synchronous.
2671-
// TODO: However, to avoid waterfalls, we should not defer if this render
2672-
// was itself spawned by an earlier useDeferredValue. Plan is to add a
2673-
// Deferred lane to track this.
2674+
initialValue !== undefined &&
2675+
// However, to avoid waterfalls, we do not defer if this render
2676+
// was itself spawned by an earlier useDeferredValue. Check if DeferredLane
2677+
// is part of the render lanes.
2678+
!includesSomeLane(renderLanes, DeferredLane)
2679+
) {
2680+
// Render with the initial value
26742681
hook.memoizedState = initialValue;
26752682

2676-
// Schedule a deferred render
2677-
const deferredLane = claimNextTransitionLane();
2683+
// Schedule a deferred render to switch to the final value.
2684+
const deferredLane = requestDeferredLane();
26782685
currentlyRenderingFiber.lanes = mergeLanes(
26792686
currentlyRenderingFiber.lanes,
26802687
deferredLane,
@@ -2710,7 +2717,7 @@ function updateDeferredValueImpl<T>(
27102717

27112718
if (!is(value, prevValue)) {
27122719
// Schedule a deferred render
2713-
const deferredLane = claimNextTransitionLane();
2720+
const deferredLane = requestDeferredLane();
27142721
currentlyRenderingFiber.lanes = mergeLanes(
27152722
currentlyRenderingFiber.lanes,
27162723
deferredLane,

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,11 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
619619
}
620620
}
621621

622-
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
622+
export function markRootSuspended(
623+
root: FiberRoot,
624+
suspendedLanes: Lanes,
625+
spawnedLane: Lane,
626+
) {
623627
root.suspendedLanes |= suspendedLanes;
624628
root.pingedLanes &= ~suspendedLanes;
625629

@@ -634,13 +638,21 @@ export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
634638

635639
lanes &= ~lane;
636640
}
641+
642+
if (spawnedLane !== NoLane) {
643+
markSpawnedDeferredLane(root, spawnedLane, suspendedLanes);
644+
}
637645
}
638646

639647
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
640648
root.pingedLanes |= root.suspendedLanes & pingedLanes;
641649
}
642650

643-
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
651+
export function markRootFinished(
652+
root: FiberRoot,
653+
remainingLanes: Lanes,
654+
spawnedLane: Lane,
655+
) {
644656
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
645657

646658
root.pendingLanes = remainingLanes;
@@ -686,6 +698,37 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
686698

687699
lanes &= ~lane;
688700
}
701+
702+
if (spawnedLane !== NoLane) {
703+
markSpawnedDeferredLane(
704+
root,
705+
spawnedLane,
706+
// This render finished successfully without suspending, so we don't need
707+
// to entangle the spawned task with the parent task.
708+
NoLanes,
709+
);
710+
}
711+
}
712+
713+
function markSpawnedDeferredLane(
714+
root: FiberRoot,
715+
spawnedLane: Lane,
716+
entangledLanes: Lanes,
717+
) {
718+
// This render spawned a deferred task. Mark it as pending.
719+
root.pendingLanes |= spawnedLane;
720+
root.suspendedLanes &= ~spawnedLane;
721+
722+
// Entangle the spawned lane with the DeferredLane bit so that we know it
723+
// was the result of another render. This lets us avoid a useDeferredValue
724+
// waterfall — only the first level will defer.
725+
const spawnedLaneIndex = laneToIndex(spawnedLane);
726+
root.entangledLanes |= spawnedLane;
727+
root.entanglements[spawnedLaneIndex] |=
728+
DeferredLane |
729+
// If the parent render task suspended, we must also entangle those lanes
730+
// with the spawned task.
731+
entangledLanes;
689732
}
690733

691734
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
366366
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
367367
// Lanes that were pinged (in an interleaved event) during this render.
368368
let workInProgressRootPingedLanes: Lanes = NoLanes;
369+
// If this lane scheduled deferred work, this is the lane of the deferred task.
370+
let workInProgressDeferredLane: Lane = NoLane;
369371
// Errors that are thrown during the render phase.
370372
let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
371373
null;
@@ -683,6 +685,27 @@ function requestRetryLane(fiber: Fiber) {
683685
return claimNextRetryLane();
684686
}
685687

688+
export function requestDeferredLane(): Lane {
689+
if (workInProgressDeferredLane === NoLane) {
690+
// If there are multiple useDeferredValue hooks in the same render, the
691+
// tasks that they spawn should all be batched together, so they should all
692+
// receive the same lane.
693+
if (includesOnlyRetries(workInProgressRootRenderLanes)) {
694+
// Retries are slightly lower priority than transitions, so if Retry task
695+
// spawns a deferred task, the deferred task is also considered a Retry.
696+
workInProgressDeferredLane = claimNextRetryLane();
697+
} else if (includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)) {
698+
// There's only one OffscreenLane, so if it contains deferred work, we
699+
// should just reschedule using the same lane.
700+
workInProgressDeferredLane = OffscreenLane;
701+
} else {
702+
// Everything else is spawned as a transition.
703+
workInProgressDeferredLane = requestTransitionLane();
704+
}
705+
}
706+
return workInProgressDeferredLane;
707+
}
708+
686709
export function scheduleUpdateOnFiber(
687710
root: FiberRoot,
688711
fiber: Fiber,
@@ -712,7 +735,11 @@ export function scheduleUpdateOnFiber(
712735
// The incoming update might unblock the current render. Interrupt the
713736
// current attempt and restart from the top.
714737
prepareFreshStack(root, NoLanes);
715-
markRootSuspended(root, workInProgressRootRenderLanes);
738+
markRootSuspended(
739+
root,
740+
workInProgressRootRenderLanes,
741+
workInProgressDeferredLane,
742+
);
716743
}
717744

718745
// Mark that the root has a pending update.
@@ -792,7 +819,11 @@ export function scheduleUpdateOnFiber(
792819
// effect of interrupting the current render and switching to the update.
793820
// TODO: Make sure this doesn't override pings that happen while we've
794821
// already started rendering.
795-
markRootSuspended(root, workInProgressRootRenderLanes);
822+
markRootSuspended(
823+
root,
824+
workInProgressRootRenderLanes,
825+
workInProgressDeferredLane,
826+
);
796827
}
797828
}
798829

@@ -903,7 +934,7 @@ export function performConcurrentWorkOnRoot(
903934
// The render unwound without completing the tree. This happens in special
904935
// cases where need to exit the current render without producing a
905936
// consistent tree or committing.
906-
markRootSuspended(root, lanes);
937+
markRootSuspended(root, lanes, NoLane);
907938
} else {
908939
// The render completed.
909940

@@ -947,7 +978,7 @@ export function performConcurrentWorkOnRoot(
947978
if (exitStatus === RootFatalErrored) {
948979
const fatalError = workInProgressRootFatalError;
949980
prepareFreshStack(root, NoLanes);
950-
markRootSuspended(root, lanes);
981+
markRootSuspended(root, lanes, NoLane);
951982
ensureRootIsScheduled(root);
952983
throw fatalError;
953984
}
@@ -1074,7 +1105,7 @@ function finishConcurrentRender(
10741105
// This is a transition, so we should exit without committing a
10751106
// placeholder and without scheduling a timeout. Delay indefinitely
10761107
// until we receive more data.
1077-
markRootSuspended(root, lanes);
1108+
markRootSuspended(root, lanes, workInProgressDeferredLane);
10781109
return;
10791110
}
10801111
// Commit the placeholder.
@@ -1096,6 +1127,7 @@ function finishConcurrentRender(
10961127
root,
10971128
workInProgressRootRecoverableErrors,
10981129
workInProgressTransitions,
1130+
workInProgressDeferredLane,
10991131
);
11001132
} else {
11011133
if (
@@ -1109,7 +1141,7 @@ function finishConcurrentRender(
11091141

11101142
// Don't bother with a very short suspense time.
11111143
if (msUntilTimeout > 10) {
1112-
markRootSuspended(root, lanes);
1144+
markRootSuspended(root, lanes, workInProgressDeferredLane);
11131145

11141146
const nextLanes = getNextLanes(root, NoLanes);
11151147
if (nextLanes !== NoLanes) {
@@ -1131,6 +1163,7 @@ function finishConcurrentRender(
11311163
workInProgressRootRecoverableErrors,
11321164
workInProgressTransitions,
11331165
lanes,
1166+
workInProgressDeferredLane,
11341167
),
11351168
msUntilTimeout,
11361169
);
@@ -1143,6 +1176,7 @@ function finishConcurrentRender(
11431176
workInProgressRootRecoverableErrors,
11441177
workInProgressTransitions,
11451178
lanes,
1179+
workInProgressDeferredLane,
11461180
);
11471181
}
11481182
}
@@ -1153,6 +1187,7 @@ function commitRootWhenReady(
11531187
recoverableErrors: Array<CapturedValue<mixed>> | null,
11541188
transitions: Array<Transition> | null,
11551189
lanes: Lanes,
1190+
spawnedLane: Lane,
11561191
) {
11571192
// TODO: Combine retry throttling with Suspensey commits. Right now they run
11581193
// one after the other.
@@ -1180,13 +1215,13 @@ function commitRootWhenReady(
11801215
root.cancelPendingCommit = schedulePendingCommit(
11811216
commitRoot.bind(null, root, recoverableErrors, transitions),
11821217
);
1183-
markRootSuspended(root, lanes);
1218+
markRootSuspended(root, lanes, spawnedLane);
11841219
return;
11851220
}
11861221
}
11871222

11881223
// Otherwise, commit immediately.
1189-
commitRoot(root, recoverableErrors, transitions);
1224+
commitRoot(root, recoverableErrors, transitions, spawnedLane);
11901225
}
11911226

11921227
function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
@@ -1242,7 +1277,11 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
12421277
return true;
12431278
}
12441279

1245-
function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
1280+
function markRootSuspended(
1281+
root: FiberRoot,
1282+
suspendedLanes: Lanes,
1283+
spawnedLane: Lane,
1284+
) {
12461285
// When suspending, we should always exclude lanes that were pinged or (more
12471286
// rarely, since we try to avoid it) updated during the render phase.
12481287
// TODO: Lol maybe there's a better way to factor this besides this
@@ -1252,7 +1291,7 @@ function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
12521291
suspendedLanes,
12531292
workInProgressRootInterleavedUpdatedLanes,
12541293
);
1255-
markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
1294+
markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes, spawnedLane);
12561295
}
12571296

12581297
// This is the entry point for synchronous tasks that don't go
@@ -1302,7 +1341,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
13021341
if (exitStatus === RootFatalErrored) {
13031342
const fatalError = workInProgressRootFatalError;
13041343
prepareFreshStack(root, NoLanes);
1305-
markRootSuspended(root, lanes);
1344+
markRootSuspended(root, lanes, NoLane);
13061345
ensureRootIsScheduled(root);
13071346
throw fatalError;
13081347
}
@@ -1311,7 +1350,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
13111350
// The render unwound without completing the tree. This happens in special
13121351
// cases where need to exit the current render without producing a
13131352
// consistent tree or committing.
1314-
markRootSuspended(root, lanes);
1353+
markRootSuspended(root, lanes, NoLane);
13151354
ensureRootIsScheduled(root);
13161355
return null;
13171356
}
@@ -1325,6 +1364,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
13251364
root,
13261365
workInProgressRootRecoverableErrors,
13271366
workInProgressTransitions,
1367+
workInProgressDeferredLane,
13281368
);
13291369

13301370
// Before exiting, make sure there's a callback scheduled for the next
@@ -1537,6 +1577,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
15371577
workInProgressRootInterleavedUpdatedLanes = NoLanes;
15381578
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
15391579
workInProgressRootPingedLanes = NoLanes;
1580+
workInProgressDeferredLane = NoLane;
15401581
workInProgressRootConcurrentErrors = null;
15411582
workInProgressRootRecoverableErrors = null;
15421583

@@ -1808,9 +1849,9 @@ export function renderDidSuspendDelayIfPossible(): void {
18081849
// Check if there are updates that we skipped tree that might have unblocked
18091850
// this render.
18101851
if (
1811-
workInProgressRoot !== null &&
18121852
(includesNonIdleWork(workInProgressRootSkippedLanes) ||
1813-
includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes))
1853+
includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes)) &&
1854+
workInProgressRoot !== null
18141855
) {
18151856
// Mark the current render as suspended so that we switch to working on
18161857
// the updates that were skipped. Usually we only suspend at the end of
@@ -1821,8 +1862,11 @@ export function renderDidSuspendDelayIfPossible(): void {
18211862
// pinged or updated while we were rendering.
18221863
// TODO: Consider unwinding immediately, using the
18231864
// SuspendedOnHydration mechanism.
1824-
// $FlowFixMe[incompatible-call] need null check workInProgressRoot
1825-
markRootSuspended(workInProgressRoot, workInProgressRootRenderLanes);
1865+
markRootSuspended(
1866+
workInProgressRoot,
1867+
workInProgressRootRenderLanes,
1868+
workInProgressDeferredLane,
1869+
);
18261870
}
18271871
}
18281872

@@ -2592,6 +2636,7 @@ function commitRoot(
25922636
root: FiberRoot,
25932637
recoverableErrors: null | Array<CapturedValue<mixed>>,
25942638
transitions: Array<Transition> | null,
2639+
spawnedLane: Lane,
25952640
) {
25962641
// TODO: This no longer makes any sense. We already wrap the mutation and
25972642
// layout phases. Should be able to remove.
@@ -2606,6 +2651,7 @@ function commitRoot(
26062651
recoverableErrors,
26072652
transitions,
26082653
previousUpdateLanePriority,
2654+
spawnedLane,
26092655
);
26102656
} finally {
26112657
ReactCurrentBatchConfig.transition = prevTransition;
@@ -2620,6 +2666,7 @@ function commitRootImpl(
26202666
recoverableErrors: null | Array<CapturedValue<mixed>>,
26212667
transitions: Array<Transition> | null,
26222668
renderPriorityLevel: EventPriority,
2669+
spawnedLane: Lane,
26232670
) {
26242671
do {
26252672
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
@@ -2696,7 +2743,7 @@ function commitRootImpl(
26962743
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
26972744
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
26982745

2699-
markRootFinished(root, remainingLanes);
2746+
markRootFinished(root, remainingLanes, spawnedLane);
27002747

27012748
if (root === workInProgressRoot) {
27022749
// We can reset these now that they are finished.

0 commit comments

Comments
 (0)