From b2a68a65c84b63ac86930d88ae5c84380cbbdeb6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 17 Oct 2023 12:48:11 -0400 Subject: [PATCH] useDeferredValue should skip initialValue if it suspends (#27509) ### Based on https://github.com/facebook/react/pull/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. --- .../src/__tests__/TimelineProfiler-test.js | 52 ++-- .../src/__tests__/preprocessData-test.js | 24 +- .../react-reconciler/src/ReactFiberHooks.js | 23 +- .../react-reconciler/src/ReactFiberLane.js | 82 ++++-- .../src/ReactFiberWorkLoop.js | 81 ++++-- .../__tests__/DebugTracing-test.internal.js | 2 +- .../src/__tests__/ReactDeferredValue-test.js | 275 ++++++++++++++++++ 7 files changed, 458 insertions(+), 81 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js index cd9081d8b95b2..23ad7f657ba33 100644 --- a/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js +++ b/packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js @@ -153,7 +153,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-2", "--layout-effects-stop", "--commit-stop", @@ -183,7 +183,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -244,7 +244,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-2", "--layout-effects-stop", "--commit-stop", @@ -286,7 +286,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-2", "--layout-effects-stop", "--commit-stop", @@ -337,7 +337,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -392,7 +392,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -446,7 +446,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--schedule-state-update-2-Example", "--layout-effects-stop", @@ -459,7 +459,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--commit-stop", "--commit-stop", ] @@ -499,7 +499,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--schedule-forced-update-2-Example", "--layout-effects-stop", @@ -512,7 +512,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--commit-stop", "--commit-stop", ] @@ -564,7 +564,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -618,7 +618,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -658,7 +658,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--component-layout-effect-mount-start-Example", "--schedule-state-update-2-Example", @@ -673,7 +673,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--commit-stop", "--commit-stop", ] @@ -705,7 +705,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -723,7 +723,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--commit-stop", ] `); @@ -755,7 +755,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--layout-effects-stop", "--commit-stop", @@ -805,7 +805,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-2", "--schedule-state-update-2-ErrorBoundary", "--layout-effects-stop", @@ -819,7 +819,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--commit-stop", ] `); @@ -885,7 +885,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--schedule-state-update-2-ErrorBoundary", "--layout-effects-stop", @@ -898,7 +898,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--commit-stop", "--commit-stop", ] @@ -961,7 +961,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-32", "--component-layout-effect-mount-start-ComponentWithEffects", "--component-layout-effect-mount-stop", @@ -1017,7 +1017,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--component-layout-effect-unmount-start-ComponentWithEffects", "--component-layout-effect-unmount-stop", "--component-layout-effect-unmount-start-ComponentWithEffects", @@ -1051,7 +1051,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-2", "--layout-effects-stop", "--commit-stop", @@ -1101,7 +1101,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-2", "--layout-effects-stop", "--commit-stop", @@ -1141,7 +1141,7 @@ describe('Timeline profiler', () => { "--profiler-version-1", "--react-internal-module-start- at filtered (:0:0)", "--react-internal-module-stop- at filtered (:1:1)", - "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen", + "--react-lane-labels-SyncHydrationLane,Sync,InputContinuousHydration,InputContinuous,DefaultHydration,Default,TransitionHydration,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Transition,Retry,Retry,Retry,Retry,SelectiveHydration,IdleHydration,Idle,Offscreen,Deferred", "--layout-effects-start-8", "--layout-effects-stop", "--commit-stop", diff --git a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js index b822663c93861..52eb869e9d17a 100644 --- a/packages/react-devtools-shared/src/__tests__/preprocessData-test.js +++ b/packages/react-devtools-shared/src/__tests__/preprocessData-test.js @@ -2001,15 +2001,15 @@ describe('Timeline profiler', () => { 524288 => "Transition", 1048576 => "Transition", 2097152 => "Transition", - 4194304 => "Transition", + 4194304 => "Retry", 8388608 => "Retry", 16777216 => "Retry", 33554432 => "Retry", - 67108864 => "Retry", - 134217728 => "SelectiveHydration", - 268435456 => "IdleHydration", - 536870912 => "Idle", - 1073741824 => "Offscreen", + 67108864 => "SelectiveHydration", + 134217728 => "IdleHydration", + 268435456 => "Idle", + 536870912 => "Offscreen", + 1073741824 => "Deferred", }, "laneToReactMeasureMap": Map { 1 => [], @@ -2269,15 +2269,15 @@ describe('Timeline profiler', () => { 524288 => "Transition", 1048576 => "Transition", 2097152 => "Transition", - 4194304 => "Transition", + 4194304 => "Retry", 8388608 => "Retry", 16777216 => "Retry", 33554432 => "Retry", - 67108864 => "Retry", - 134217728 => "SelectiveHydration", - 268435456 => "IdleHydration", - 536870912 => "Idle", - 1073741824 => "Offscreen", + 67108864 => "SelectiveHydration", + 134217728 => "IdleHydration", + 268435456 => "Idle", + 536870912 => "Offscreen", + 1073741824 => "Deferred", }, "laneToReactMeasureMap": Map { 1 => [], diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index d2c456a5f9c9a..c3c85af085932 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -61,16 +61,17 @@ import { NoLane, SyncLane, OffscreenLane, + DeferredLane, NoLanes, isSubsetOfLanes, includesBlockingLane, includesOnlyNonUrgentLanes, - claimNextTransitionLane, mergeLanes, removeLanes, intersectLanes, isTransitionLane, markRootEntangled, + includesSomeLane, } from './ReactFiberLane'; import { ContinuousEventPriority, @@ -101,6 +102,7 @@ import { getWorkInProgressRootRenderLanes, scheduleUpdateOnFiber, requestUpdateLane, + requestDeferredLane, markSkippedUpdateLanes, isInvalidExecutionContextForEventFunction, } from './ReactFiberWorkLoop'; @@ -2665,16 +2667,21 @@ function rerenderDeferredValue(value: T, initialValue?: T): T { } function mountDeferredValueImpl(hook: Hook, value: T, initialValue?: T): T { - if (enableUseDeferredValueInitialArg && initialValue !== undefined) { + if ( + enableUseDeferredValueInitialArg && // When `initialValue` is provided, we defer the initial render even if the // current render is not synchronous. - // TODO: However, to avoid waterfalls, we should not defer if this render - // was itself spawned by an earlier useDeferredValue. Plan is to add a - // Deferred lane to track this. + initialValue !== undefined && + // However, to avoid waterfalls, we do not defer if this render + // was itself spawned by an earlier useDeferredValue. Check if DeferredLane + // is part of the render lanes. + !includesSomeLane(renderLanes, DeferredLane) + ) { + // Render with the initial value hook.memoizedState = initialValue; - // Schedule a deferred render - const deferredLane = claimNextTransitionLane(); + // Schedule a deferred render to switch to the final value. + const deferredLane = requestDeferredLane(); currentlyRenderingFiber.lanes = mergeLanes( currentlyRenderingFiber.lanes, deferredLane, @@ -2710,7 +2717,7 @@ function updateDeferredValueImpl( if (!is(value, prevValue)) { // Schedule a deferred render - const deferredLane = claimNextTransitionLane(); + const deferredLane = requestDeferredLane(); currentlyRenderingFiber.lanes = mergeLanes( currentlyRenderingFiber.lanes, deferredLane, diff --git a/packages/react-reconciler/src/ReactFiberLane.js b/packages/react-reconciler/src/ReactFiberLane.js index 21055ac82f7e9..864edf6ee676b 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -52,7 +52,7 @@ export const SyncUpdateLanes: Lane = enableUnifiedSyncLane : SyncLane; const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000; -const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000; +const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000; const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000; const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000; const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000; @@ -68,24 +68,24 @@ const TransitionLane12: Lane = /* */ 0b0000000000001000000 const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000; const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000; const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000; -const TransitionLane16: Lane = /* */ 0b0000000010000000000000000000000; -const RetryLanes: Lanes = /* */ 0b0000111100000000000000000000000; -const RetryLane1: Lane = /* */ 0b0000000100000000000000000000000; -const RetryLane2: Lane = /* */ 0b0000001000000000000000000000000; -const RetryLane3: Lane = /* */ 0b0000010000000000000000000000000; -const RetryLane4: Lane = /* */ 0b0000100000000000000000000000000; +const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000; +const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000; +const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000; +const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000; +const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000; export const SomeRetryLane: Lane = RetryLane1; -export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; +export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000; -const NonIdleLanes: Lanes = /* */ 0b0001111111111111111111111111111; +const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111; -export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000; -export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000; +export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000; +export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000; -export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000; +export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000; +export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000; // Any lane that might schedule an update. This is used to detect infinite // update loops, so it doesn't include hydration lanes or retries. @@ -135,6 +135,9 @@ export function getLabelForLane(lane: Lane): string | void { if (lane & OffscreenLane) { return 'Offscreen'; } + if (lane & DeferredLane) { + return 'Deferred'; + } } } @@ -180,7 +183,6 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { case TransitionLane13: case TransitionLane14: case TransitionLane15: - case TransitionLane16: return lanes & TransitionLanes; case RetryLane1: case RetryLane2: @@ -195,6 +197,10 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes { return IdleLane; case OffscreenLane: return OffscreenLane; + case DeferredLane: + // This shouldn't be reachable because deferred work is always entangled + // with something else. + return NoLanes; default: if (__DEV__) { console.error( @@ -367,7 +373,6 @@ function computeExpirationTime(lane: Lane, currentTime: number) { case TransitionLane13: case TransitionLane14: case TransitionLane15: - case TransitionLane16: return currentTime + 5000; case RetryLane1: case RetryLane2: @@ -383,6 +388,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) { case IdleHydrationLane: case IdleLane: case OffscreenLane: + case DeferredLane: // Anything idle priority or lower should never expire. return NoTimestamp; default: @@ -616,7 +622,11 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) { } } -export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { +export function markRootSuspended( + root: FiberRoot, + suspendedLanes: Lanes, + spawnedLane: Lane, +) { root.suspendedLanes |= suspendedLanes; root.pingedLanes &= ~suspendedLanes; @@ -631,13 +641,21 @@ export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { lanes &= ~lane; } + + if (spawnedLane !== NoLane) { + markSpawnedDeferredLane(root, spawnedLane, suspendedLanes); + } } export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) { root.pingedLanes |= root.suspendedLanes & pingedLanes; } -export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { +export function markRootFinished( + root: FiberRoot, + remainingLanes: Lanes, + spawnedLane: Lane, +) { const noLongerPendingLanes = root.pendingLanes & ~remainingLanes; root.pendingLanes = remainingLanes; @@ -683,6 +701,37 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { lanes &= ~lane; } + + if (spawnedLane !== NoLane) { + markSpawnedDeferredLane( + root, + spawnedLane, + // This render finished successfully without suspending, so we don't need + // to entangle the spawned task with the parent task. + NoLanes, + ); + } +} + +function markSpawnedDeferredLane( + root: FiberRoot, + spawnedLane: Lane, + entangledLanes: Lanes, +) { + // This render spawned a deferred task. Mark it as pending. + root.pendingLanes |= spawnedLane; + root.suspendedLanes &= ~spawnedLane; + + // Entangle the spawned lane with the DeferredLane bit so that we know it + // was the result of another render. This lets us avoid a useDeferredValue + // waterfall — only the first level will defer. + const spawnedLaneIndex = laneToIndex(spawnedLane); + root.entangledLanes |= spawnedLane; + root.entanglements[spawnedLaneIndex] |= + DeferredLane | + // If the parent render task suspended, we must also entangle those lanes + // with the spawned task. + entangledLanes; } export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) { @@ -795,7 +844,6 @@ export function getBumpedLaneForHydration( case TransitionLane13: case TransitionLane14: case TransitionLane15: - case TransitionLane16: case RetryLane1: case RetryLane2: case RetryLane3: diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index cb217d04eba19..fbfa153b71785 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -366,6 +366,8 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes; let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes; // Lanes that were pinged (in an interleaved event) during this render. let workInProgressRootPingedLanes: Lanes = NoLanes; +// If this lane scheduled deferred work, this is the lane of the deferred task. +let workInProgressDeferredLane: Lane = NoLane; // Errors that are thrown during the render phase. let workInProgressRootConcurrentErrors: Array> | null = null; @@ -683,6 +685,27 @@ function requestRetryLane(fiber: Fiber) { return claimNextRetryLane(); } +export function requestDeferredLane(): Lane { + if (workInProgressDeferredLane === NoLane) { + // If there are multiple useDeferredValue hooks in the same render, the + // tasks that they spawn should all be batched together, so they should all + // receive the same lane. + if (includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)) { + // There's only one OffscreenLane, so if it contains deferred work, we + // should just reschedule using the same lane. + // TODO: We also use OffscreenLane for hydration, on the basis that the + // initial HTML is the same as the hydrated UI, but since the deferred + // task will change the UI, it should be treated like an update. Use + // TransitionHydrationLane to trigger selective hydration. + workInProgressDeferredLane = OffscreenLane; + } else { + // Everything else is spawned as a transition. + workInProgressDeferredLane = requestTransitionLane(); + } + } + return workInProgressDeferredLane; +} + export function scheduleUpdateOnFiber( root: FiberRoot, fiber: Fiber, @@ -712,7 +735,11 @@ export function scheduleUpdateOnFiber( // The incoming update might unblock the current render. Interrupt the // current attempt and restart from the top. prepareFreshStack(root, NoLanes); - markRootSuspended(root, workInProgressRootRenderLanes); + markRootSuspended( + root, + workInProgressRootRenderLanes, + workInProgressDeferredLane, + ); } // Mark that the root has a pending update. @@ -792,7 +819,11 @@ export function scheduleUpdateOnFiber( // effect of interrupting the current render and switching to the update. // TODO: Make sure this doesn't override pings that happen while we've // already started rendering. - markRootSuspended(root, workInProgressRootRenderLanes); + markRootSuspended( + root, + workInProgressRootRenderLanes, + workInProgressDeferredLane, + ); } } @@ -903,7 +934,7 @@ export function performConcurrentWorkOnRoot( // The render unwound without completing the tree. This happens in special // cases where need to exit the current render without producing a // consistent tree or committing. - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, NoLane); } else { // The render completed. @@ -947,7 +978,7 @@ export function performConcurrentWorkOnRoot( if (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, NoLane); ensureRootIsScheduled(root); throw fatalError; } @@ -1074,7 +1105,7 @@ function finishConcurrentRender( // This is a transition, so we should exit without committing a // placeholder and without scheduling a timeout. Delay indefinitely // until we receive more data. - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, workInProgressDeferredLane); return; } // Commit the placeholder. @@ -1096,6 +1127,7 @@ function finishConcurrentRender( root, workInProgressRootRecoverableErrors, workInProgressTransitions, + workInProgressDeferredLane, ); } else { if ( @@ -1109,7 +1141,7 @@ function finishConcurrentRender( // Don't bother with a very short suspense time. if (msUntilTimeout > 10) { - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, workInProgressDeferredLane); const nextLanes = getNextLanes(root, NoLanes); if (nextLanes !== NoLanes) { @@ -1131,6 +1163,7 @@ function finishConcurrentRender( workInProgressRootRecoverableErrors, workInProgressTransitions, lanes, + workInProgressDeferredLane, ), msUntilTimeout, ); @@ -1143,6 +1176,7 @@ function finishConcurrentRender( workInProgressRootRecoverableErrors, workInProgressTransitions, lanes, + workInProgressDeferredLane, ); } } @@ -1153,6 +1187,7 @@ function commitRootWhenReady( recoverableErrors: Array> | null, transitions: Array | null, lanes: Lanes, + spawnedLane: Lane, ) { // TODO: Combine retry throttling with Suspensey commits. Right now they run // one after the other. @@ -1180,13 +1215,13 @@ function commitRootWhenReady( root.cancelPendingCommit = schedulePendingCommit( commitRoot.bind(null, root, recoverableErrors, transitions), ); - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, spawnedLane); return; } } // Otherwise, commit immediately. - commitRoot(root, recoverableErrors, transitions); + commitRoot(root, recoverableErrors, transitions, spawnedLane); } function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean { @@ -1242,7 +1277,11 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean { return true; } -function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { +function markRootSuspended( + root: FiberRoot, + suspendedLanes: Lanes, + spawnedLane: Lane, +) { // When suspending, we should always exclude lanes that were pinged or (more // rarely, since we try to avoid it) updated during the render phase. // TODO: Lol maybe there's a better way to factor this besides this @@ -1252,7 +1291,7 @@ function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) { suspendedLanes, workInProgressRootInterleavedUpdatedLanes, ); - markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes); + markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes, spawnedLane); } // This is the entry point for synchronous tasks that don't go @@ -1302,7 +1341,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { if (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, NoLane); ensureRootIsScheduled(root); throw fatalError; } @@ -1311,7 +1350,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { // The render unwound without completing the tree. This happens in special // cases where need to exit the current render without producing a // consistent tree or committing. - markRootSuspended(root, lanes); + markRootSuspended(root, lanes, NoLane); ensureRootIsScheduled(root); return null; } @@ -1325,6 +1364,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null { root, workInProgressRootRecoverableErrors, workInProgressTransitions, + workInProgressDeferredLane, ); // Before exiting, make sure there's a callback scheduled for the next @@ -1537,6 +1577,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { workInProgressRootInterleavedUpdatedLanes = NoLanes; workInProgressRootRenderPhaseUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; + workInProgressDeferredLane = NoLane; workInProgressRootConcurrentErrors = null; workInProgressRootRecoverableErrors = null; @@ -1808,9 +1849,9 @@ export function renderDidSuspendDelayIfPossible(): void { // Check if there are updates that we skipped tree that might have unblocked // this render. if ( - workInProgressRoot !== null && (includesNonIdleWork(workInProgressRootSkippedLanes) || - includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes)) + includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes)) && + workInProgressRoot !== null ) { // Mark the current render as suspended so that we switch to working on // the updates that were skipped. Usually we only suspend at the end of @@ -1821,8 +1862,11 @@ export function renderDidSuspendDelayIfPossible(): void { // pinged or updated while we were rendering. // TODO: Consider unwinding immediately, using the // SuspendedOnHydration mechanism. - // $FlowFixMe[incompatible-call] need null check workInProgressRoot - markRootSuspended(workInProgressRoot, workInProgressRootRenderLanes); + markRootSuspended( + workInProgressRoot, + workInProgressRootRenderLanes, + workInProgressDeferredLane, + ); } } @@ -2592,6 +2636,7 @@ function commitRoot( root: FiberRoot, recoverableErrors: null | Array>, transitions: Array | null, + spawnedLane: Lane, ) { // TODO: This no longer makes any sense. We already wrap the mutation and // layout phases. Should be able to remove. @@ -2606,6 +2651,7 @@ function commitRoot( recoverableErrors, transitions, previousUpdateLanePriority, + spawnedLane, ); } finally { ReactCurrentBatchConfig.transition = prevTransition; @@ -2620,6 +2666,7 @@ function commitRootImpl( recoverableErrors: null | Array>, transitions: Array | null, renderPriorityLevel: EventPriority, + spawnedLane: Lane, ) { do { // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which @@ -2696,7 +2743,7 @@ function commitRootImpl( const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes(); remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes); - markRootFinished(root, remainingLanes); + markRootFinished(root, remainingLanes, spawnedLane); if (root === workInProgressRoot) { // We can reset these now that they are finished. diff --git a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js index 39d42d254f610..f8026d6ea9fab 100644 --- a/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js +++ b/packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js @@ -19,7 +19,7 @@ describe('DebugTracing', () => { const SYNC_LANE_STRING = '0b0000000000000000000000000000010'; const DEFAULT_LANE_STRING = '0b0000000000000000000000000100000'; - const RETRY_LANE_STRING = '0b0000000100000000000000000000000'; + const RETRY_LANE_STRING = '0b0000000010000000000000000000000'; global.IS_REACT_ACT_ENVIRONMENT = true; diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index c29a9c4287275..76d90f39cf276 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -15,8 +15,11 @@ let startTransition; let useDeferredValue; let useMemo; let useState; +let Suspense; +let Offscreen; let assertLog; let waitForPaint; +let textCache; describe('ReactDeferredValue', () => { beforeEach(() => { @@ -30,17 +33,78 @@ describe('ReactDeferredValue', () => { useDeferredValue = React.useDeferredValue; useMemo = React.useMemo; useState = React.useState; + Suspense = React.Suspense; + Offscreen = React.unstable_Offscreen; const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; waitForPaint = InternalTestUtils.waitForPaint; + + textCache = new Map(); }); + function resolveText(text) { + const record = textCache.get(text); + if (record === undefined) { + const newRecord = { + status: 'resolved', + value: text, + }; + textCache.set(text, newRecord); + } else if (record.status === 'pending') { + const thenable = record.value; + record.status = 'resolved'; + record.value = text; + thenable.pings.forEach(t => t()); + } + } + + function readText(text) { + const record = textCache.get(text); + if (record !== undefined) { + switch (record.status) { + case 'pending': + Scheduler.log(`Suspend! [${text}]`); + throw record.value; + case 'rejected': + throw record.value; + case 'resolved': + return record.value; + } + } else { + Scheduler.log(`Suspend! [${text}]`); + const thenable = { + pings: [], + then(resolve) { + if (newRecord.status === 'pending') { + thenable.pings.push(resolve); + } else { + Promise.resolve().then(() => resolve(newRecord.value)); + } + }, + }; + + const newRecord = { + status: 'pending', + value: thenable, + }; + textCache.set(text, newRecord); + + throw thenable; + } + } + function Text({text}) { Scheduler.log(text); return text; } + function AsyncText({text}) { + readText(text); + Scheduler.log(text); + return text; + } + it('does not cause an infinite defer loop if the original value isn\t memoized', async () => { function App({value}) { // The object passed to useDeferredValue is never the same as the previous @@ -341,4 +405,215 @@ describe('ReactDeferredValue', () => { assertLog(['Final']); expect(root).toMatchRenderedOutput('Final'); }); + + // @gate enableUseDeferredValueInitialArg + it( + 'if a suspended render spawns a deferred task, we can switch to the ' + + 'deferred task without finishing the original one', + async () => { + function App() { + const text = useDeferredValue('Final', 'Loading...'); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog([ + 'Suspend! [Loading...]', + // The initial value suspended, so we attempt the final value, which + // also suspends. + 'Suspend! [Final]', + ]); + expect(root).toMatchRenderedOutput(null); + + // The final value loads, so we can skip the initial value entirely. + await act(() => resolveText('Final')); + assertLog(['Final']); + expect(root).toMatchRenderedOutput('Final'); + + // When the initial value finally loads, nothing happens because we no + // longer need it. + await act(() => resolveText('Loading...')); + assertLog([]); + expect(root).toMatchRenderedOutput('Final'); + }, + ); + + // @gate enableUseDeferredValueInitialArg + it( + 'if a suspended render spawns a deferred task that also suspends, we can ' + + 'finish the original task if that one loads first', + async () => { + function App() { + const text = useDeferredValue('Final', 'Loading...'); + return ; + } + + const root = ReactNoop.createRoot(); + await act(() => root.render()); + assertLog([ + 'Suspend! [Loading...]', + // The initial value suspended, so we attempt the final value, which + // also suspends. + 'Suspend! [Final]', + ]); + expect(root).toMatchRenderedOutput(null); + + // The initial value resolves first, so we render that. + await act(() => resolveText('Loading...')); + assertLog([ + 'Loading...', + // Still waiting for the final value. + 'Suspend! [Final]', + ]); + expect(root).toMatchRenderedOutput('Loading...'); + + // The final value loads, so we can switch to that. + await act(() => resolveText('Final')); + assertLog(['Final']); + expect(root).toMatchRenderedOutput('Final'); + }, + ); + + // @gate enableUseDeferredValueInitialArg + it( + 'if there are multiple useDeferredValues in the same tree, only the ' + + 'first level defers; subsequent ones go straight to the final value, to ' + + 'avoid a waterfall', + async () => { + function App() { + const showContent = useDeferredValue(true, false); + if (!showContent) { + return ; + } + return ; + } + + function Content() { + const text = useDeferredValue('Content', 'Content Preview'); + return ; + } + + const root = ReactNoop.createRoot(); + resolveText('App Preview'); + + await act(() => root.render()); + assertLog([ + // The App shows an immediate preview + 'App Preview', + // Then we switch to showing the content. The Content component also + // contains a useDeferredValue, but since we already showed a preview + // in a parent component, we skip the preview in the inner one and + // go straight to attempting the final value. + // + // (Note that this is intentionally different from how nested Suspense + // boundaries work, where we always prefer to show the innermost + // loading state.) + 'Suspend! [Content]', + ]); + // Still showing the App preview state because the inner + // content suspended. + expect(root).toMatchRenderedOutput('App Preview'); + + // Finish loading the content + await act(() => resolveText('Content')); + // We didn't even attempt to render Content Preview. + assertLog(['Content']); + expect(root).toMatchRenderedOutput('Content'); + }, + ); + + // @gate enableUseDeferredValueInitialArg + it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => { + // Same as the previous test but with a Suspense boundary separating the + // two useDeferredValue hooks. + function App() { + const showContent = useDeferredValue(true, false); + if (!showContent) { + return ; + } + return ( + }> + + + ); + } + + function Content() { + const text = useDeferredValue('Content', 'Content Preview'); + return ; + } + + const root = ReactNoop.createRoot(); + resolveText('App Preview'); + + await act(() => root.render()); + assertLog([ + // The App shows an immediate preview + 'App Preview', + // Then we switch to showing the content. The Content component also + // contains a useDeferredValue, but since we already showed a preview + // in a parent component, we skip the preview in the inner one and + // go straight to attempting the final value. + 'Suspend! [Content]', + 'Loading...', + ]); + // The content suspended, so we show a Suspense fallback + expect(root).toMatchRenderedOutput('Loading...'); + + // Finish loading the content + await act(() => resolveText('Content')); + // We didn't even attempt to render Content Preview. + assertLog(['Content']); + expect(root).toMatchRenderedOutput('Content'); + }); + + // @gate enableUseDeferredValueInitialArg + // @gate enableOffscreen + it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => { + function App() { + const text = useDeferredValue('Final', 'Preview'); + return ( +
+ +
+ ); + } + + let revealContent; + function Container({children}) { + const [shouldShow, setState] = useState(false); + revealContent = () => setState(true); + return ( + + {children} + + ); + } + + const root = ReactNoop.createRoot(); + + // Prerender a hidden tree + resolveText('Preview'); + await act(() => + root.render( + + + , + ), + ); + assertLog(['Preview', 'Suspend! [Final]']); + expect(root).toMatchRenderedOutput(); + + // Finish loading the content + await act(() => resolveText('Final')); + assertLog(['Final']); + expect(root).toMatchRenderedOutput(); + + // Now reveal the hidden tree. It should toggle the visibility without + // having to re-render anything inside the prerendered tree. + await act(() => revealContent()); + assertLog([]); + expect(root).toMatchRenderedOutput(
Final
); + }); });