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.
Andrew Clark committed
Oct 17, 2023 at 12:48 UTC
b2a68a65c84b63ac86930d88ae5c84380cbbdeb6
7 files changed
+458
-81
packages/react-devtools-shared/src/__tests__/TimelineProfiler-test.js
+26
-26
@@ -153,7 +153,7 @@ describe('Timeline profiler', () => {
153
"--profiler-version-1",
154
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
155
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
156
- "--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",
156
+ "--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",
157
"--layout-effects-start-2",
158
"--layout-effects-stop",
159
"--commit-stop",
@@ -183,7 +183,7 @@ describe('Timeline profiler', () => {
183
"--profiler-version-1",
184
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
185
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
186
- "--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",
186
+ "--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",
187
"--layout-effects-start-32",
188
"--layout-effects-stop",
189
"--commit-stop",
@@ -244,7 +244,7 @@ describe('Timeline profiler', () => {
244
"--profiler-version-1",
245
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
246
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
247
- "--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",
247
+ "--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",
248
"--layout-effects-start-2",
249
"--layout-effects-stop",
250
"--commit-stop",
@@ -286,7 +286,7 @@ describe('Timeline profiler', () => {
286
"--profiler-version-1",
287
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
288
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
289
- "--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",
289
+ "--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",
290
"--layout-effects-start-2",
291
"--layout-effects-stop",
292
"--commit-stop",
@@ -337,7 +337,7 @@ describe('Timeline profiler', () => {
337
"--profiler-version-1",
338
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
339
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
340
- "--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",
340
+ "--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",
341
"--layout-effects-start-32",
342
"--layout-effects-stop",
343
"--commit-stop",
@@ -392,7 +392,7 @@ describe('Timeline profiler', () => {
392
"--profiler-version-1",
393
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
394
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
395
- "--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",
395
+ "--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",
396
"--layout-effects-start-32",
397
"--layout-effects-stop",
398
"--commit-stop",
@@ -446,7 +446,7 @@ describe('Timeline profiler', () => {
446
"--profiler-version-1",
447
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
448
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
449
- "--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",
449
+ "--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",
450
"--layout-effects-start-32",
451
"--schedule-state-update-2-Example",
452
"--layout-effects-stop",
@@ -459,7 +459,7 @@ describe('Timeline profiler', () => {
459
"--profiler-version-1",
460
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
461
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
462
- "--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",
462
+ "--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",
463
"--commit-stop",
464
"--commit-stop",
465
]
@@ -499,7 +499,7 @@ describe('Timeline profiler', () => {
499
"--profiler-version-1",
500
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
501
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
502
- "--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",
502
+ "--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",
503
"--layout-effects-start-32",
504
"--schedule-forced-update-2-Example",
505
"--layout-effects-stop",
@@ -512,7 +512,7 @@ describe('Timeline profiler', () => {
512
"--profiler-version-1",
513
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
514
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
515
- "--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",
515
+ "--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",
516
"--commit-stop",
517
"--commit-stop",
518
]
@@ -564,7 +564,7 @@ describe('Timeline profiler', () => {
564
"--profiler-version-1",
565
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
566
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
567
- "--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",
567
+ "--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",
568
"--layout-effects-start-32",
569
"--layout-effects-stop",
570
"--commit-stop",
@@ -618,7 +618,7 @@ describe('Timeline profiler', () => {
618
"--profiler-version-1",
619
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
620
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
621
- "--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",
621
+ "--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",
622
"--layout-effects-start-32",
623
"--layout-effects-stop",
624
"--commit-stop",
@@ -658,7 +658,7 @@ describe('Timeline profiler', () => {
658
"--profiler-version-1",
659
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
660
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
661
- "--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",
661
+ "--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",
662
"--layout-effects-start-32",
663
"--component-layout-effect-mount-start-Example",
664
"--schedule-state-update-2-Example",
@@ -673,7 +673,7 @@ describe('Timeline profiler', () => {
673
"--profiler-version-1",
674
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
675
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
676
- "--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",
676
+ "--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",
677
"--commit-stop",
678
"--commit-stop",
679
]
@@ -705,7 +705,7 @@ describe('Timeline profiler', () => {
705
"--profiler-version-1",
706
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
707
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
708
- "--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",
708
+ "--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",
709
"--layout-effects-start-32",
710
"--layout-effects-stop",
711
"--commit-stop",
@@ -723,7 +723,7 @@ describe('Timeline profiler', () => {
723
"--profiler-version-1",
724
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
725
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
726
- "--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",
726
+ "--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",
727
"--commit-stop",
728
]
729
`);
@@ -755,7 +755,7 @@ describe('Timeline profiler', () => {
755
"--profiler-version-1",
756
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
757
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
758
- "--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",
758
+ "--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",
759
"--layout-effects-start-32",
760
"--layout-effects-stop",
761
"--commit-stop",
@@ -805,7 +805,7 @@ describe('Timeline profiler', () => {
805
"--profiler-version-1",
806
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
807
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
808
- "--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",
808
+ "--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",
809
"--layout-effects-start-2",
810
"--schedule-state-update-2-ErrorBoundary",
811
"--layout-effects-stop",
@@ -819,7 +819,7 @@ describe('Timeline profiler', () => {
819
"--profiler-version-1",
820
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
821
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
822
- "--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",
822
+ "--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",
823
"--commit-stop",
824
]
825
`);
@@ -885,7 +885,7 @@ describe('Timeline profiler', () => {
885
"--profiler-version-1",
886
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
887
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
888
- "--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",
888
+ "--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",
889
"--layout-effects-start-32",
890
"--schedule-state-update-2-ErrorBoundary",
891
"--layout-effects-stop",
@@ -898,7 +898,7 @@ describe('Timeline profiler', () => {
898
"--profiler-version-1",
899
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
900
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
901
- "--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",
901
+ "--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",
902
"--commit-stop",
903
"--commit-stop",
904
]
@@ -961,7 +961,7 @@ describe('Timeline profiler', () => {
961
"--profiler-version-1",
962
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
963
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
964
- "--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",
964
+ "--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",
965
"--layout-effects-start-32",
966
"--component-layout-effect-mount-start-ComponentWithEffects",
967
"--component-layout-effect-mount-stop",
@@ -1017,7 +1017,7 @@ describe('Timeline profiler', () => {
1017
"--profiler-version-1",
1018
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
1019
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
1020
- "--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",
1020
+ "--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",
1021
"--component-layout-effect-unmount-start-ComponentWithEffects",
1022
"--component-layout-effect-unmount-stop",
1023
"--component-layout-effect-unmount-start-ComponentWithEffects",
@@ -1051,7 +1051,7 @@ describe('Timeline profiler', () => {
1051
"--profiler-version-1",
1052
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
1053
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
1054
- "--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",
1054
+ "--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",
1055
"--layout-effects-start-2",
1056
"--layout-effects-stop",
1057
"--commit-stop",
@@ -1101,7 +1101,7 @@ describe('Timeline profiler', () => {
1101
"--profiler-version-1",
1102
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
1103
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
1104
- "--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",
1104
+ "--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",
1105
"--layout-effects-start-2",
1106
"--layout-effects-stop",
1107
"--commit-stop",
@@ -1141,7 +1141,7 @@ describe('Timeline profiler', () => {
1141
"--profiler-version-1",
1142
"--react-internal-module-start- at filtered (<anonymous>:0:0)",
1143
"--react-internal-module-stop- at filtered (<anonymous>:1:1)",
1144
- "--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",
1144
+ "--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",
1145
"--layout-effects-start-8",
1146
"--layout-effects-stop",
1147
"--commit-stop",
packages/react-devtools-shared/src/__tests__/preprocessData-test.js
+12
-12
@@ -2001,15 +2001,15 @@ describe('Timeline profiler', () => {
2001
524288 => "Transition",
2002
1048576 => "Transition",
2003
2097152 => "Transition",
2004
- 4194304 => "Transition",
2004
+ 4194304 => "Retry",
2005
8388608 => "Retry",
2006
16777216 => "Retry",
2007
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",
2013
},
2014
"laneToReactMeasureMap": Map {
2015
1 => [],
@@ -2269,15 +2269,15 @@ describe('Timeline profiler', () => {
2269
524288 => "Transition",
2270
1048576 => "Transition",
2271
2097152 => "Transition",
2272
- 4194304 => "Transition",
2272
+ 4194304 => "Retry",
2273
8388608 => "Retry",
2274
16777216 => "Retry",
2275
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",
2281
},
2282
"laneToReactMeasureMap": Map {
2283
1 => [],
packages/react-reconciler/src/ReactFiberHooks.js
+15
-8
@@ -61,16 +61,17 @@ import {
61
NoLane,
62
SyncLane,
63
OffscreenLane,
64
+ DeferredLane,
65
NoLanes,
66
isSubsetOfLanes,
67
includesBlockingLane,
68
includesOnlyNonUrgentLanes,
68
- claimNextTransitionLane,
69
mergeLanes,
70
removeLanes,
71
intersectLanes,
72
isTransitionLane,
73
markRootEntangled,
74
+ includesSomeLane,
75
} from './ReactFiberLane';
76
import {
77
ContinuousEventPriority,
@@ -101,6 +102,7 @@ import {
102
getWorkInProgressRootRenderLanes,
103
scheduleUpdateOnFiber,
104
requestUpdateLane,
105
+ requestDeferredLane,
106
markSkippedUpdateLanes,
107
isInvalidExecutionContextForEventFunction,
108
} from './ReactFiberWorkLoop';
@@ -2665,16 +2667,21 @@ function rerenderDeferredValue<T>(value: T, initialValue?: T): T {
2667
}
2668
2669
function mountDeferredValueImpl<T>(hook: Hook, value: T, initialValue?: T): T {
2668
- if (enableUseDeferredValueInitialArg && initialValue !== undefined) {
2670
+ if (
2671
+ enableUseDeferredValueInitialArg &&
2672
// When `initialValue` is provided, we defer the initial render even if the
2673
// 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
2681
hook.memoizedState = initialValue;
2682
2676
- // Schedule a deferred render
2677
- const deferredLane = claimNextTransitionLane();
2683
+ // Schedule a deferred render to switch to the final value.
2684
+ const deferredLane = requestDeferredLane();
2685
currentlyRenderingFiber.lanes = mergeLanes(
2686
currentlyRenderingFiber.lanes,
2687
deferredLane,
@@ -2710,7 +2717,7 @@ function updateDeferredValueImpl<T>(
2717
2718
if (!is(value, prevValue)) {
2719
// Schedule a deferred render
2713
- const deferredLane = claimNextTransitionLane();
2720
+ const deferredLane = requestDeferredLane();
2721
currentlyRenderingFiber.lanes = mergeLanes(
2722
currentlyRenderingFiber.lanes,
2723
deferredLane,
packages/react-reconciler/src/ReactFiberLane.js
+65
-17
@@ -52,7 +52,7 @@ export const SyncUpdateLanes: Lane = enableUnifiedSyncLane
52
: SyncLane;
53
54
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
55
-const TransitionLanes: Lanes = /* */ 0b0000000011111111111111110000000;
55
+const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
56
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
57
const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
58
const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
@@ -68,24 +68,24 @@ const TransitionLane12: Lane = /* */ 0b0000000000001000000
68
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
69
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
70
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
71
-const TransitionLane16: Lane = /* */ 0b0000000010000000000000000000000;
71
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;
77
78
export const SomeRetryLane: Lane = RetryLane1;
79
81
-export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
80
+export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
81
83
-const NonIdleLanes: Lanes = /* */ 0b0001111111111111111111111111111;
82
+const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111;
83
85
-export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
86
-export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;
84
+export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
85
+export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
86
88
-export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
87
+export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
88
+export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
89
90
// Any lane that might schedule an update. This is used to detect infinite
91
// update loops, so it doesn't include hydration lanes or retries.
@@ -135,6 +135,9 @@ export function getLabelForLane(lane: Lane): string | void {
135
if (lane & OffscreenLane) {
136
return 'Offscreen';
137
}
138
+ if (lane & DeferredLane) {
139
+ return 'Deferred';
140
+ }
141
}
142
}
143
@@ -180,7 +183,6 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
183
case TransitionLane13:
184
case TransitionLane14:
185
case TransitionLane15:
183
- case TransitionLane16:
186
return lanes & TransitionLanes;
187
case RetryLane1:
188
case RetryLane2:
@@ -195,6 +197,10 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
197
return IdleLane;
198
case OffscreenLane:
199
return OffscreenLane;
200
+ case DeferredLane:
201
+ // This shouldn't be reachable because deferred work is always entangled
202
+ // with something else.
203
+ return NoLanes;
204
default:
205
if (__DEV__) {
206
console.error(
@@ -367,7 +373,6 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
373
case TransitionLane13:
374
case TransitionLane14:
375
case TransitionLane15:
370
- case TransitionLane16:
376
return currentTime + 5000;
377
case RetryLane1:
378
case RetryLane2:
@@ -383,6 +388,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
388
case IdleHydrationLane:
389
case IdleLane:
390
case OffscreenLane:
391
+ case DeferredLane:
392
// Anything idle priority or lower should never expire.
393
return NoTimestamp;
394
default:
@@ -616,7 +622,11 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
622
}
623
}
624
619
-export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
625
+export function markRootSuspended(
626
+ root: FiberRoot,
627
+ suspendedLanes: Lanes,
628
+ spawnedLane: Lane,
629
+) {
630
root.suspendedLanes |= suspendedLanes;
631
root.pingedLanes &= ~suspendedLanes;
632
@@ -631,13 +641,21 @@ export function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
641
642
lanes &= ~lane;
643
}
644
+
645
+ if (spawnedLane !== NoLane) {
646
+ markSpawnedDeferredLane(root, spawnedLane, suspendedLanes);
647
+ }
648
}
649
650
export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
651
root.pingedLanes |= root.suspendedLanes & pingedLanes;
652
}
653
640
-export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
654
+export function markRootFinished(
655
+ root: FiberRoot,
656
+ remainingLanes: Lanes,
657
+ spawnedLane: Lane,
658
+) {
659
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
660
661
root.pendingLanes = remainingLanes;
@@ -683,6 +701,37 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
701
702
lanes &= ~lane;
703
}
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;
735
}
736
737
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
@@ -795,7 +844,6 @@ export function getBumpedLaneForHydration(
844
case TransitionLane13:
845
case TransitionLane14:
846
case TransitionLane15:
798
- case TransitionLane16:
847
case RetryLane1:
848
case RetryLane2:
849
case RetryLane3:
packages/react-reconciler/src/ReactFiberWorkLoop.js
+64
-17
@@ -366,6 +366,8 @@ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
366
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
367
// Lanes that were pinged (in an interleaved event) during this render.
368
let workInProgressRootPingedLanes: Lanes = NoLanes;
369
+// If this lane scheduled deferred work, this is the lane of the deferred task.
370
+let workInProgressDeferredLane: Lane = NoLane;
371
// Errors that are thrown during the render phase.
372
let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
373
null;
@@ -683,6 +685,27 @@ function requestRetryLane(fiber: Fiber) {
685
return claimNextRetryLane();
686
}
687
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 (includesSomeLane(workInProgressRootRenderLanes, OffscreenLane)) {
694
+ // There's only one OffscreenLane, so if it contains deferred work, we
695
+ // should just reschedule using the same lane.
696
+ // TODO: We also use OffscreenLane for hydration, on the basis that the
697
+ // initial HTML is the same as the hydrated UI, but since the deferred
698
+ // task will change the UI, it should be treated like an update. Use
699
+ // TransitionHydrationLane to trigger selective hydration.
700
+ workInProgressDeferredLane = OffscreenLane;
701
+ } else {
702
+ // Everything else is spawned as a transition.
703
+ workInProgressDeferredLane = requestTransitionLane();
704
+ }
705
+ }
706
+ return workInProgressDeferredLane;
707
+}
708
+
709
export function scheduleUpdateOnFiber(
710
root: FiberRoot,
711
fiber: Fiber,
@@ -712,7 +735,11 @@ export function scheduleUpdateOnFiber(
735
// The incoming update might unblock the current render. Interrupt the
736
// current attempt and restart from the top.
737
prepareFreshStack(root, NoLanes);
715
- markRootSuspended(root, workInProgressRootRenderLanes);
738
+ markRootSuspended(
739
+ root,
740
+ workInProgressRootRenderLanes,
741
+ workInProgressDeferredLane,
742
+ );
743
}
744
745
// Mark that the root has a pending update.
@@ -792,7 +819,11 @@ export function scheduleUpdateOnFiber(
819
// effect of interrupting the current render and switching to the update.
820
// TODO: Make sure this doesn't override pings that happen while we've
821
// already started rendering.
795
- markRootSuspended(root, workInProgressRootRenderLanes);
822
+ markRootSuspended(
823
+ root,
824
+ workInProgressRootRenderLanes,
825
+ workInProgressDeferredLane,
826
+ );
827
}
828
}
829
@@ -903,7 +934,7 @@ export function performConcurrentWorkOnRoot(
934
// The render unwound without completing the tree. This happens in special
935
// cases where need to exit the current render without producing a
936
// consistent tree or committing.
906
- markRootSuspended(root, lanes);
937
+ markRootSuspended(root, lanes, NoLane);
938
} else {
939
// The render completed.
940
@@ -947,7 +978,7 @@ export function performConcurrentWorkOnRoot(
978
if (exitStatus === RootFatalErrored) {
979
const fatalError = workInProgressRootFatalError;
980
prepareFreshStack(root, NoLanes);
950
- markRootSuspended(root, lanes);
981
+ markRootSuspended(root, lanes, NoLane);
982
ensureRootIsScheduled(root);
983
throw fatalError;
984
}
@@ -1074,7 +1105,7 @@ function finishConcurrentRender(
1105
// This is a transition, so we should exit without committing a
1106
// placeholder and without scheduling a timeout. Delay indefinitely
1107
// until we receive more data.
1077
- markRootSuspended(root, lanes);
1108
+ markRootSuspended(root, lanes, workInProgressDeferredLane);
1109
return;
1110
}
1111
// Commit the placeholder.
@@ -1096,6 +1127,7 @@ function finishConcurrentRender(
1127
root,
1128
workInProgressRootRecoverableErrors,
1129
workInProgressTransitions,
1130
+ workInProgressDeferredLane,
1131
);
1132
} else {
1133
if (
@@ -1109,7 +1141,7 @@ function finishConcurrentRender(
1141
1142
// Don't bother with a very short suspense time.
1143
if (msUntilTimeout > 10) {
1112
- markRootSuspended(root, lanes);
1144
+ markRootSuspended(root, lanes, workInProgressDeferredLane);
1145
1146
const nextLanes = getNextLanes(root, NoLanes);
1147
if (nextLanes !== NoLanes) {
@@ -1131,6 +1163,7 @@ function finishConcurrentRender(
1163
workInProgressRootRecoverableErrors,
1164
workInProgressTransitions,
1165
lanes,
1166
+ workInProgressDeferredLane,
1167
),
1168
msUntilTimeout,
1169
);
@@ -1143,6 +1176,7 @@ function finishConcurrentRender(
1176
workInProgressRootRecoverableErrors,
1177
workInProgressTransitions,
1178
lanes,
1179
+ workInProgressDeferredLane,
1180
);
1181
}
1182
}
@@ -1153,6 +1187,7 @@ function commitRootWhenReady(
1187
recoverableErrors: Array<CapturedValue<mixed>> | null,
1188
transitions: Array<Transition> | null,
1189
lanes: Lanes,
1190
+ spawnedLane: Lane,
1191
) {
1192
// TODO: Combine retry throttling with Suspensey commits. Right now they run
1193
// one after the other.
@@ -1180,13 +1215,13 @@ function commitRootWhenReady(
1215
root.cancelPendingCommit = schedulePendingCommit(
1216
commitRoot.bind(null, root, recoverableErrors, transitions),
1217
);
1183
- markRootSuspended(root, lanes);
1218
+ markRootSuspended(root, lanes, spawnedLane);
1219
return;
1220
}
1221
}
1222
1223
// Otherwise, commit immediately.
1189
- commitRoot(root, recoverableErrors, transitions);
1224
+ commitRoot(root, recoverableErrors, transitions, spawnedLane);
1225
}
1226
1227
function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
@@ -1242,7 +1277,11 @@ function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
1277
return true;
1278
}
1279
1245
-function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
1280
+function markRootSuspended(
1281
+ root: FiberRoot,
1282
+ suspendedLanes: Lanes,
1283
+ spawnedLane: Lane,
1284
+) {
1285
// When suspending, we should always exclude lanes that were pinged or (more
1286
// rarely, since we try to avoid it) updated during the render phase.
1287
// TODO: Lol maybe there's a better way to factor this besides this
@@ -1252,7 +1291,7 @@ function markRootSuspended(root: FiberRoot, suspendedLanes: Lanes) {
1291
suspendedLanes,
1292
workInProgressRootInterleavedUpdatedLanes,
1293
);
1255
- markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
1294
+ markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes, spawnedLane);
1295
}
1296
1297
// This is the entry point for synchronous tasks that don't go
@@ -1302,7 +1341,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1341
if (exitStatus === RootFatalErrored) {
1342
const fatalError = workInProgressRootFatalError;
1343
prepareFreshStack(root, NoLanes);
1305
- markRootSuspended(root, lanes);
1344
+ markRootSuspended(root, lanes, NoLane);
1345
ensureRootIsScheduled(root);
1346
throw fatalError;
1347
}
@@ -1311,7 +1350,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1350
// The render unwound without completing the tree. This happens in special
1351
// cases where need to exit the current render without producing a
1352
// consistent tree or committing.
1314
- markRootSuspended(root, lanes);
1353
+ markRootSuspended(root, lanes, NoLane);
1354
ensureRootIsScheduled(root);
1355
return null;
1356
}
@@ -1325,6 +1364,7 @@ export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
1364
root,
1365
workInProgressRootRecoverableErrors,
1366
workInProgressTransitions,
1367
+ workInProgressDeferredLane,
1368
);
1369
1370
// Before exiting, make sure there's a callback scheduled for the next
@@ -1537,6 +1577,7 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1577
workInProgressRootInterleavedUpdatedLanes = NoLanes;
1578
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
1579
workInProgressRootPingedLanes = NoLanes;
1580
+ workInProgressDeferredLane = NoLane;
1581
workInProgressRootConcurrentErrors = null;
1582
workInProgressRootRecoverableErrors = null;
1583
@@ -1808,9 +1849,9 @@ export function renderDidSuspendDelayIfPossible(): void {
1849
// Check if there are updates that we skipped tree that might have unblocked
1850
// this render.
1851
if (
1811
- workInProgressRoot !== null &&
1852
(includesNonIdleWork(workInProgressRootSkippedLanes) ||
1813
- includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes))
1853
+ includesNonIdleWork(workInProgressRootInterleavedUpdatedLanes)) &&
1854
+ workInProgressRoot !== null
1855
) {
1856
// Mark the current render as suspended so that we switch to working on
1857
// the updates that were skipped. Usually we only suspend at the end of
@@ -1821,8 +1862,11 @@ export function renderDidSuspendDelayIfPossible(): void {
1862
// pinged or updated while we were rendering.
1863
// TODO: Consider unwinding immediately, using the
1864
// SuspendedOnHydration mechanism.
1824
- // $FlowFixMe[incompatible-call] need null check workInProgressRoot
1825
- markRootSuspended(workInProgressRoot, workInProgressRootRenderLanes);
1865
+ markRootSuspended(
1866
+ workInProgressRoot,
1867
+ workInProgressRootRenderLanes,
1868
+ workInProgressDeferredLane,
1869
+ );
1870
}
1871
}
1872
@@ -2592,6 +2636,7 @@ function commitRoot(
2636
root: FiberRoot,
2637
recoverableErrors: null | Array<CapturedValue<mixed>>,
2638
transitions: Array<Transition> | null,
2639
+ spawnedLane: Lane,
2640
) {
2641
// TODO: This no longer makes any sense. We already wrap the mutation and
2642
// layout phases. Should be able to remove.
@@ -2606,6 +2651,7 @@ function commitRoot(
2651
recoverableErrors,
2652
transitions,
2653
previousUpdateLanePriority,
2654
+ spawnedLane,
2655
);
2656
} finally {
2657
ReactCurrentBatchConfig.transition = prevTransition;
@@ -2620,6 +2666,7 @@ function commitRootImpl(
2666
recoverableErrors: null | Array<CapturedValue<mixed>>,
2667
transitions: Array<Transition> | null,
2668
renderPriorityLevel: EventPriority,
2669
+ spawnedLane: Lane,
2670
) {
2671
do {
2672
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
@@ -2696,7 +2743,7 @@ function commitRootImpl(
2743
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
2744
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
2745
2699
- markRootFinished(root, remainingLanes);
2746
+ markRootFinished(root, remainingLanes, spawnedLane);
2747
2748
if (root === workInProgressRoot) {
2749
// We can reset these now that they are finished.
packages/react-reconciler/src/__tests__/DebugTracing-test.internal.js
+1
-1
@@ -19,7 +19,7 @@ describe('DebugTracing', () => {
19
20
const SYNC_LANE_STRING = '0b0000000000000000000000000000010';
21
const DEFAULT_LANE_STRING = '0b0000000000000000000000000100000';
22
- const RETRY_LANE_STRING = '0b0000000100000000000000000000000';
22
+ const RETRY_LANE_STRING = '0b0000000010000000000000000000000';
23
24
global.IS_REACT_ACT_ENVIRONMENT = true;
25
packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
+275
@@ -15,8 +15,11 @@ let startTransition;
15
let useDeferredValue;
16
let useMemo;
17
let useState;
18
+let Suspense;
19
+let Offscreen;
20
let assertLog;
21
let waitForPaint;
22
+let textCache;
23
24
describe('ReactDeferredValue', () => {
25
beforeEach(() => {
@@ -30,17 +33,78 @@ describe('ReactDeferredValue', () => {
33
useDeferredValue = React.useDeferredValue;
34
useMemo = React.useMemo;
35
useState = React.useState;
36
+ Suspense = React.Suspense;
37
+ Offscreen = React.unstable_Offscreen;
38
39
const InternalTestUtils = require('internal-test-utils');
40
assertLog = InternalTestUtils.assertLog;
41
waitForPaint = InternalTestUtils.waitForPaint;
42
+
43
+ textCache = new Map();
44
});
45
46
+ function resolveText(text) {
47
+ const record = textCache.get(text);
48
+ if (record === undefined) {
49
+ const newRecord = {
50
+ status: 'resolved',
51
+ value: text,
52
+ };
53
+ textCache.set(text, newRecord);
54
+ } else if (record.status === 'pending') {
55
+ const thenable = record.value;
56
+ record.status = 'resolved';
57
+ record.value = text;
58
+ thenable.pings.forEach(t => t());
59
+ }
60
+ }
61
+
62
+ function readText(text) {
63
+ const record = textCache.get(text);
64
+ if (record !== undefined) {
65
+ switch (record.status) {
66
+ case 'pending':
67
+ Scheduler.log(`Suspend! [${text}]`);
68
+ throw record.value;
69
+ case 'rejected':
70
+ throw record.value;
71
+ case 'resolved':
72
+ return record.value;
73
+ }
74
+ } else {
75
+ Scheduler.log(`Suspend! [${text}]`);
76
+ const thenable = {
77
+ pings: [],
78
+ then(resolve) {
79
+ if (newRecord.status === 'pending') {
80
+ thenable.pings.push(resolve);
81
+ } else {
82
+ Promise.resolve().then(() => resolve(newRecord.value));
83
+ }
84
+ },
85
+ };
86
+
87
+ const newRecord = {
88
+ status: 'pending',
89
+ value: thenable,
90
+ };
91
+ textCache.set(text, newRecord);
92
+
93
+ throw thenable;
94
+ }
95
+ }
96
+
97
function Text({text}) {
98
Scheduler.log(text);
99
return text;
100
}
101
102
+ function AsyncText({text}) {
103
+ readText(text);
104
+ Scheduler.log(text);
105
+ return text;
106
+ }
107
+
108
it('does not cause an infinite defer loop if the original value isn\t memoized', async () => {
109
function App({value}) {
110
// The object passed to useDeferredValue is never the same as the previous
@@ -341,4 +405,215 @@ describe('ReactDeferredValue', () => {
405
assertLog(['Final']);
406
expect(root).toMatchRenderedOutput('Final');
407
});
408
+
409
+ // @gate enableUseDeferredValueInitialArg
410
+ it(
411
+ 'if a suspended render spawns a deferred task, we can switch to the ' +
412
+ 'deferred task without finishing the original one',
413
+ async () => {
414
+ function App() {
415
+ const text = useDeferredValue('Final', 'Loading...');
416
+ return <AsyncText text={text} />;
417
+ }
418
+
419
+ const root = ReactNoop.createRoot();
420
+ await act(() => root.render(<App />));
421
+ assertLog([
422
+ 'Suspend! [Loading...]',
423
+ // The initial value suspended, so we attempt the final value, which
424
+ // also suspends.
425
+ 'Suspend! [Final]',
426
+ ]);
427
+ expect(root).toMatchRenderedOutput(null);
428
+
429
+ // The final value loads, so we can skip the initial value entirely.
430
+ await act(() => resolveText('Final'));
431
+ assertLog(['Final']);
432
+ expect(root).toMatchRenderedOutput('Final');
433
+
434
+ // When the initial value finally loads, nothing happens because we no
435
+ // longer need it.
436
+ await act(() => resolveText('Loading...'));
437
+ assertLog([]);
438
+ expect(root).toMatchRenderedOutput('Final');
439
+ },
440
+ );
441
+
442
+ // @gate enableUseDeferredValueInitialArg
443
+ it(
444
+ 'if a suspended render spawns a deferred task that also suspends, we can ' +
445
+ 'finish the original task if that one loads first',
446
+ async () => {
447
+ function App() {
448
+ const text = useDeferredValue('Final', 'Loading...');
449
+ return <AsyncText text={text} />;
450
+ }
451
+
452
+ const root = ReactNoop.createRoot();
453
+ await act(() => root.render(<App />));
454
+ assertLog([
455
+ 'Suspend! [Loading...]',
456
+ // The initial value suspended, so we attempt the final value, which
457
+ // also suspends.
458
+ 'Suspend! [Final]',
459
+ ]);
460
+ expect(root).toMatchRenderedOutput(null);
461
+
462
+ // The initial value resolves first, so we render that.
463
+ await act(() => resolveText('Loading...'));
464
+ assertLog([
465
+ 'Loading...',
466
+ // Still waiting for the final value.
467
+ 'Suspend! [Final]',
468
+ ]);
469
+ expect(root).toMatchRenderedOutput('Loading...');
470
+
471
+ // The final value loads, so we can switch to that.
472
+ await act(() => resolveText('Final'));
473
+ assertLog(['Final']);
474
+ expect(root).toMatchRenderedOutput('Final');
475
+ },
476
+ );
477
+
478
+ // @gate enableUseDeferredValueInitialArg
479
+ it(
480
+ 'if there are multiple useDeferredValues in the same tree, only the ' +
481
+ 'first level defers; subsequent ones go straight to the final value, to ' +
482
+ 'avoid a waterfall',
483
+ async () => {
484
+ function App() {
485
+ const showContent = useDeferredValue(true, false);
486
+ if (!showContent) {
487
+ return <Text text="App Preview" />;
488
+ }
489
+ return <Content />;
490
+ }
491
+
492
+ function Content() {
493
+ const text = useDeferredValue('Content', 'Content Preview');
494
+ return <AsyncText text={text} />;
495
+ }
496
+
497
+ const root = ReactNoop.createRoot();
498
+ resolveText('App Preview');
499
+
500
+ await act(() => root.render(<App />));
501
+ assertLog([
502
+ // The App shows an immediate preview
503
+ 'App Preview',
504
+ // Then we switch to showing the content. The Content component also
505
+ // contains a useDeferredValue, but since we already showed a preview
506
+ // in a parent component, we skip the preview in the inner one and
507
+ // go straight to attempting the final value.
508
+ //
509
+ // (Note that this is intentionally different from how nested Suspense
510
+ // boundaries work, where we always prefer to show the innermost
511
+ // loading state.)
512
+ 'Suspend! [Content]',
513
+ ]);
514
+ // Still showing the App preview state because the inner
515
+ // content suspended.
516
+ expect(root).toMatchRenderedOutput('App Preview');
517
+
518
+ // Finish loading the content
519
+ await act(() => resolveText('Content'));
520
+ // We didn't even attempt to render Content Preview.
521
+ assertLog(['Content']);
522
+ expect(root).toMatchRenderedOutput('Content');
523
+ },
524
+ );
525
+
526
+ // @gate enableUseDeferredValueInitialArg
527
+ it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
528
+ // Same as the previous test but with a Suspense boundary separating the
529
+ // two useDeferredValue hooks.
530
+ function App() {
531
+ const showContent = useDeferredValue(true, false);
532
+ if (!showContent) {
533
+ return <Text text="App Preview" />;
534
+ }
535
+ return (
536
+ <Suspense fallback={<Text text="Loading..." />}>
537
+ <Content />
538
+ </Suspense>
539
+ );
540
+ }
541
+
542
+ function Content() {
543
+ const text = useDeferredValue('Content', 'Content Preview');
544
+ return <AsyncText text={text} />;
545
+ }
546
+
547
+ const root = ReactNoop.createRoot();
548
+ resolveText('App Preview');
549
+
550
+ await act(() => root.render(<App />));
551
+ assertLog([
552
+ // The App shows an immediate preview
553
+ 'App Preview',
554
+ // Then we switch to showing the content. The Content component also
555
+ // contains a useDeferredValue, but since we already showed a preview
556
+ // in a parent component, we skip the preview in the inner one and
557
+ // go straight to attempting the final value.
558
+ 'Suspend! [Content]',
559
+ 'Loading...',
560
+ ]);
561
+ // The content suspended, so we show a Suspense fallback
562
+ expect(root).toMatchRenderedOutput('Loading...');
563
+
564
+ // Finish loading the content
565
+ await act(() => resolveText('Content'));
566
+ // We didn't even attempt to render Content Preview.
567
+ assertLog(['Content']);
568
+ expect(root).toMatchRenderedOutput('Content');
569
+ });
570
+
571
+ // @gate enableUseDeferredValueInitialArg
572
+ // @gate enableOffscreen
573
+ it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => {
574
+ function App() {
575
+ const text = useDeferredValue('Final', 'Preview');
576
+ return (
577
+ <div>
578
+ <AsyncText text={text} />
579
+ </div>
580
+ );
581
+ }
582
+
583
+ let revealContent;
584
+ function Container({children}) {
585
+ const [shouldShow, setState] = useState(false);
586
+ revealContent = () => setState(true);
587
+ return (
588
+ <Offscreen mode={shouldShow ? 'visible' : 'hidden'}>
589
+ {children}
590
+ </Offscreen>
591
+ );
592
+ }
593
+
594
+ const root = ReactNoop.createRoot();
595
+
596
+ // Prerender a hidden tree
597
+ resolveText('Preview');
598
+ await act(() =>
599
+ root.render(
600
+ <Container>
601
+ <App />
602
+ </Container>,
603
+ ),
604
+ );
605
+ assertLog(['Preview', 'Suspend! [Final]']);
606
+ expect(root).toMatchRenderedOutput(<div hidden={true}>Preview</div>);
607
+
608
+ // Finish loading the content
609
+ await act(() => resolveText('Final'));
610
+ assertLog(['Final']);
611
+ expect(root).toMatchRenderedOutput(<div hidden={true}>Final</div>);
612
+
613
+ // Now reveal the hidden tree. It should toggle the visibility without
614
+ // having to re-render anything inside the prerendered tree.
615
+ await act(() => revealContent());
616
+ assertLog([]);
617
+ expect(root).toMatchRenderedOutput(<div>Final</div>);
618
+ });
619
});