@samitouri / QOS-React-1 / commits / b2a68a65c8

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 });