Skip to content

Commit b2a68a6

Browse files
authored
useDeferredValue should skip initialValue if it suspends (#27509)
### Based on #27505 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 9abf6fa commit b2a68a6

File tree

7 files changed

+458
-81
lines changed

7 files changed

+458
-81
lines changed

packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js

Lines changed: 26 additions & 26 deletions
Large diffs are not rendered by default.

packages/react-devtools-shared/src/__tests__/preprocessData-test.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,15 +2001,15 @@ describe('Timeline profiler', () => {
20012001
524288 => "Transition",
20022002
1048576 => "Transition",
20032003
2097152 => "Transition",
2004-
4194304 => "Transition",
2004+
4194304 => "Retry",
20052005
8388608 => "Retry",
20062006
16777216 => "Retry",
20072007
33554432 => "Retry",
2008-
67108864 => "Retry",
2009-
134217728 => "SelectiveHydration",
2010-
268435456 => "IdleHydration",
2011-
536870912 => "Idle",
2012-
1073741824 => "Offscreen",
2008+
67108864 => "SelectiveHydration",
2009+
134217728 => "IdleHydration",
2010+
268435456 => "Idle",
2011+
536870912 => "Offscreen",
2012+
1073741824 => "Deferred",
20132013
},
20142014
"laneToReactMeasureMap": Map {
20152015
1 => [],
@@ -2269,15 +2269,15 @@ describe('Timeline profiler', () => {
22692269
524288 => "Transition",
22702270
1048576 => "Transition",
22712271
2097152 => "Transition",
2272-
4194304 => "Transition",
2272+
4194304 => "Retry",
22732273
8388608 => "Retry",
22742274
16777216 => "Retry",
22752275
33554432 => "Retry",
2276-
67108864 => "Retry",
2277-
134217728 => "SelectiveHydration",
2278-
268435456 => "IdleHydration",
2279-
536870912 => "Idle",
2280-
1073741824 => "Offscreen",
2276+
67108864 => "SelectiveHydration",
2277+
134217728 => "IdleHydration",
2278+
268435456 => "Idle",
2279+
536870912 => "Offscreen",
2280+
1073741824 => "Deferred",
22812281
},
22822282
"laneToReactMeasureMap": Map {
22832283
1 => [],

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: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const SyncUpdateLanes: Lane = enableUnifiedSyncLane
5252
: SyncLane;
5353

5454
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
55-
const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
55+
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
5656
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
5757
const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
5858
const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
@@ -68,24 +68,24 @@ const TransitionLane12: Lane = /* */ 0b0000000000001000000
6868
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
6969
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
7070
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
71-
const TransitionLane16: Lane = /* */ 0b0000000010000000000000000000000;
7271

73-
const RetryLanes: Lanes = /* */ 0b0000111100000000000000000000000;
74-
const RetryLane1: Lane = /* */ 0b0000000100000000000000000000000;
75-
const RetryLane2: Lane = /* */ 0b0000001000000000000000000000000;
76-
const RetryLane3: Lane = /* */ 0b0000010000000000000000000000000;
77-
const RetryLane4: Lane = /* */ 0b0000100000000000000000000000000;
72+
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
73+
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
74+
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
75+
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
76+
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
7877

7978
export const SomeRetryLane: Lane = RetryLane1;
8079

81-
export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
80+
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
8281

83-
const NonIdleLanes: Lanes = /* */ 0b0001111111111111111111111111111;
82+
const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111;
8483

85-
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
86-
export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;
84+
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
85+
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
8786

88-
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
87+
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
88+
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
8989

9090
// Any lane that might schedule an update. This is used to detect infinite
9191
// update loops, so it doesn't include hydration lanes or retries.
@@ -135,6 +135,9 @@ export function getLabelForLane(lane: Lane): string | void {
135135
if (lane & OffscreenLane) {
136136
return 'Offscreen';
137137
}
138+
if (lane & DeferredLane) {
139+
return 'Deferred';
140+
}
138141
}
139142
}
140143

@@ -180,7 +183,6 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
180183
case TransitionLane13:
181184
case TransitionLane14:
182185
case TransitionLane15:
183-
case TransitionLane16:
184186
return lanes & TransitionLanes;
185187
case RetryLane1:
186188
case RetryLane2:
@@ -195,6 +197,10 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
195197
return IdleLane;
196198
case OffscreenLane:
197199
return OffscreenLane;
200+
case DeferredLane:
201+
// This shouldn't be reachable because deferred work is always entangled
202+
// with something else.
203+
return NoLanes;
198204
default:
199205
if (__DEV__) {
200206
console.error(
@@ -367,7 +373,6 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
367373
case TransitionLane13:
368374
case TransitionLane14:
369375
case TransitionLane15:
370-
case TransitionLane16:
371376
return currentTime + 5000;
372377
case RetryLane1:
373378
case RetryLane2:
@@ -383,6 +388,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
383388
case IdleHydrationLane:
384389
case IdleLane:
385390
case OffscreenLane:
391+
case DeferredLane:
386392
// Anything idle priority or lower should never expire.
387393
return NoTimestamp;
388394
default:
@@ -616,7 +622,11 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
616622
}
617623
}
618624

619-
export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
625+
export function markRootSuspended(
626+
root: FiberRoot,
627+
suspendedLanes: Lanes,
628+
spawnedLane: Lane,
629+
) {
620630
root.suspendedLanes |= suspendedLanes;
621631
root.pingedLanes &= ~suspendedLanes;
622632

@@ -631,13 +641,21 @@ export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
631641

632642
lanes &= ~lane;
633643
}
644+
645+
if (spawnedLane !== NoLane) {
646+
markSpawnedDeferredLane(root, spawnedLane, suspendedLanes);
647+
}
634648
}
635649

636650
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
637651
root.pingedLanes |= root.suspendedLanes & pingedLanes;
638652
}
639653

640-
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
654+
export function markRootFinished(
655+
root: FiberRoot,
656+
remainingLanes: Lanes,
657+
spawnedLane: Lane,
658+
) {
641659
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
642660

643661
root.pendingLanes = remainingLanes;
@@ -683,6 +701,37 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
683701

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

688737
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
@@ -795,7 +844,6 @@ export function getBumpedLaneForHydration(
795844
case TransitionLane13:
796845
case TransitionLane14:
797846
case TransitionLane15:
798-
case TransitionLane16:
799847
case RetryLane1:
800848
case RetryLane2:
801849
case RetryLane3:

0 commit comments

Comments
 (0)