289
await waitForAll(['Foo']);
290
291
// The update will suspend.
292
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
293
- React.startTransition(() => {
294
- ReactNoop.render(<Foo renderBar={true} />);
295
- });
296
- } else {
292
+ React.startTransition(() => {
293
ReactNoop.render(<Foo renderBar={true} />);
298
- }
294
+ });
295
await waitForAll([
296
'Foo',
297
'Bar',
367
});
368
369
// @gate enableLegacyCache
374
- it('continues rendering siblings after suspending', async () => {
370
+ it('when something suspends, unwinds immediately without rendering siblings', async () => {
371
// A shell is needed. The update cause it to suspend.
372
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
373
await waitForAll([]);
378
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
379
- React.startTransition(() => {
380
- ReactNoop.render(
381
- <Suspense fallback={<Text text="Loading..." />}>
382
- <Text text="A" />
383
- <AsyncText text="B" />
384
- <Text text="C" />
385
- <Text text="D" />
386
- </Suspense>,
387
- );
388
- });
389
- } else {
374
+ React.startTransition(() => {
375
ReactNoop.render(
376
<Suspense fallback={<Text text="Loading..." />}>
377
<Text text="A" />
380
<Text text="D" />
381
</Suspense>,
382
);
398
- }
383
+ });
384
+
385
// B suspends. Render a fallback
386
await waitForAll(['A', 'Suspend! [B]', 'Loading...']);
387
// Did not commit yet.
439
await waitForAll([]);
440
expect(ReactNoop).toMatchRenderedOutput(null);
441
456
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
457
- React.startTransition(() => {
458
- ReactNoop.render(<App renderContent={true} />);
459
- });
460
- } else {
442
+ React.startTransition(() => {
443
ReactNoop.render(<App renderContent={true} />);
462
- }
444
+ });
445
await waitForAll(['Suspend! [Result]', 'Loading...']);
446
expect(ReactNoop).toMatchRenderedOutput(null);
447
570
571
// @gate enableLegacyCache
572
it('keeps working on lower priority work after being pinged', async () => {
591
- // Advance the virtual time so that we're close to the edge of a bucket.
592
- ReactNoop.expire(149);
593
-
573
function App(props) {
574
return (
575
<Suspense fallback={<Text text="Loading..." />}>
583
await waitForAll([]);
584
expect(ReactNoop).toMatchRenderedOutput(null);
585
607
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
608
- React.startTransition(() => {
609
- ReactNoop.render(<App showA={true} showB={false} />);
610
- });
611
- } else {
586
+ React.startTransition(() => {
587
ReactNoop.render(<App showA={true} showB={false} />);
613
- }
588
+ });
589
await waitForAll(['Suspend! [A]', 'Loading...']);
590
expect(ReactNoop).toMatchRenderedOutput(null);
591
617
- // Advance React's virtual time by enough to fall into a new async bucket,
618
- // but not enough to expire the suspense timeout.
619
- ReactNoop.expire(120);
620
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
621
- React.startTransition(() => {
622
- ReactNoop.render(<App showA={true} showB={true} />);
623
- });
624
- } else {
592
+ React.startTransition(() => {
593
ReactNoop.render(<App showA={true} showB={true} />);
626
- }
594
+ });
595
await waitForAll(['Suspend! [A]', 'Loading...']);
596
expect(ReactNoop).toMatchRenderedOutput(null);
597
712
assertLog(['Sibling', 'Step 4']);
713
});
714
747
- // @gate enableLegacyCache
748
- it('forces an expiration after an update times out', async () => {
749
- ReactNoop.render(
750
- <Fragment>
751
- <Suspense fallback={<Text text="Loading..." />} />
752
- </Fragment>,
753
- );
754
- await waitForAll([]);
755
-
756
- ReactNoop.render(
757
- <Fragment>
758
- <Suspense fallback={<Text text="Loading..." />}>
759
- <AsyncText text="Async" />
760
- </Suspense>
761
- <Text text="Sync" />
762
- </Fragment>,
763
- );
764
-
765
- await waitForAll([
766
- // The async child suspends
767
- 'Suspend! [Async]',
768
- // Render the placeholder
769
- 'Loading...',
770
- // Continue on the sibling
771
- 'Sync',
772
- ]);
773
- // The update hasn't expired yet, so we commit nothing.
774
- expect(ReactNoop).toMatchRenderedOutput(null);
775
-
776
- // Advance both React's virtual time and Jest's timers by enough to expire
777
- // the update.
778
- ReactNoop.expire(10000);
779
- await advanceTimers(10000);
780
- // No additional rendering work is required, since we already prepared
781
- // the placeholder.
782
- assertLog([]);
783
- // Should have committed the placeholder.
784
- expect(ReactNoop).toMatchRenderedOutput(
785
- <>
786
- <span prop="Loading..." />
787
- <span prop="Sync" />
788
- </>,
789
- );
790
-
791
- // Once the promise resolves, we render the suspended view
792
- await resolveText('Async');
793
- await waitForAll(['Async']);
794
- expect(ReactNoop).toMatchRenderedOutput(
795
- <>
796
- <span prop="Async" />
797
- <span prop="Sync" />
798
- </>,
799
- );
800
- });
801
-
715
// @gate enableLegacyCache
716
it('switches to an inner fallback after suspending for a while', async () => {
717
// Advance the virtual time so that we're closer to the edge of a bucket.
847
expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading (outer)..." />);
848
});
849
937
- // @gate enableLegacyCache
938
- it('expires early by default', async () => {
939
- ReactNoop.render(
940
- <Fragment>
941
- <Suspense fallback={<Text text="Loading..." />} />
942
- </Fragment>,
943
- );
944
- await waitForAll([]);
945
-
946
- ReactNoop.render(
947
- <Fragment>
948
- <Suspense fallback={<Text text="Loading..." />}>
949
- <AsyncText text="Async" />
950
- </Suspense>
951
- <Text text="Sync" />
952
- </Fragment>,
953
- );
954
-
955
- await waitForAll([
956
- // The async child suspends
957
- 'Suspend! [Async]',
958
- 'Loading...',
959
- // Continue on the sibling
960
- 'Sync',
961
- ]);
962
- // The update hasn't expired yet, so we commit nothing.
963
- expect(ReactNoop).toMatchRenderedOutput(null);
964
-
965
- // Advance both React's virtual time and Jest's timers by enough to trigger
966
- // the timeout, but not by enough to flush the promise or reach the true
967
- // expiration time.
968
- ReactNoop.expire(2000);
969
- await advanceTimers(2000);
970
- await waitForAll([]);
971
- expect(ReactNoop).toMatchRenderedOutput(
972
- <>
973
- <span prop="Loading..." />
974
- <span prop="Sync" />
975
- </>,
976
- );
977
-
978
- // Once the promise resolves, we render the suspended view
979
- await resolveText('Async');
980
- await waitForAll(['Async']);
981
- expect(ReactNoop).toMatchRenderedOutput(
982
- <>
983
- <span prop="Async" />
984
- <span prop="Sync" />
985
- </>,
986
- );
987
- });
988
-
989
- // @gate enableLegacyCache
990
- it('does not expire for transitions', async () => {
991
- ReactNoop.render(
992
- <Fragment>
993
- <Suspense fallback={<Text text="Loading..." />} />
994
- </Fragment>,
995
- );
996
- await waitForAll([]);
997
-
998
- React.startTransition(() => {
999
- ReactNoop.render(
1000
- <Fragment>
1001
- <Suspense fallback={<Text text="Loading..." />}>
1002
- <AsyncText text="Async" />
1003
- </Suspense>
1004
- <Text text="Sync" />
1005
- </Fragment>,
1006
- );
1007
- });
1008
-
1009
- await waitForAll([
1010
- // The async child suspends
1011
- 'Suspend! [Async]',
1012
- 'Loading...',
1013
- // Continue on the sibling
1014
- 'Sync',
1015
- ]);
1016
- // The update hasn't expired yet, so we commit nothing.
1017
- expect(ReactNoop).toMatchRenderedOutput(null);
1018
-
1019
- // Advance both React's virtual time and Jest's timers,
1020
- // but not by enough to flush the promise or reach the true expiration time.
1021
- ReactNoop.expire(2000);
1022
- await advanceTimers(2000);
1023
- // Even flushing won't yield a fallback in a transition.
1024
- expect(ReactNoop).toMatchRenderedOutput(null);
1025
-
1026
- await waitForAll([]);
1027
- expect(ReactNoop).toMatchRenderedOutput(null);
1028
-
1029
- // Once the promise resolves, we render the suspended view
1030
- await resolveText('Async');
1031
- await waitForAll(['Async', 'Sync']);
1032
- expect(ReactNoop).toMatchRenderedOutput(
1033
- <>
1034
- <span prop="Async" />
1035
- <span prop="Sync" />
1036
- </>,
1037
- );
1038
- });
1039
-
850
// @gate enableLegacyCache
851
it('resolves successfully even if fallback render is pending', async () => {
852
const root = ReactNoop.createRoot();
939
ReactNoop.render(<Suspense fallback={<Text text="Loading..." />} />);
940
await waitForAll([]);
941
1132
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
1133
- React.startTransition(() => {
1134
- ReactNoop.render(
1135
- <Suspense fallback={<Text text="Loading..." />}>
1136
- <AsyncText text="Async" />
1137
- </Suspense>,
1138
- );
1139
- });
1140
- } else {
942
+ React.startTransition(() => {
943
ReactNoop.render(
944
<Suspense fallback={<Text text="Loading..." />}>
945
<AsyncText text="Async" />
946
</Suspense>,
947
);
1146
- }
948
+ });
949
await waitForAll(['Suspend! [Async]', 'Loading...']);
950
expect(ReactNoop).toMatchRenderedOutput(null);
951
1712
]);
1713
});
1714
1913
- // @gate enableLegacyCache
1914
- it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1915
- function Foo({renderContent}) {
1916
- Scheduler.log('Foo');
1917
- return (
1918
- <Suspense fallback={<Text text="Loading..." />}>
1919
- {renderContent ? <AsyncText text="A" /> : null}
1920
- </Suspense>
1921
- );
1922
- }
1923
-
1924
- ReactNoop.render(<Foo />);
1925
- await waitForAll(['Foo']);
1926
-
1927
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
1928
- React.startTransition(() => {
1929
- ReactNoop.render(<Foo renderContent={true} />);
1930
- });
1931
- } else {
1932
- ReactNoop.render(<Foo renderContent={true} />);
1933
- }
1934
- Scheduler.unstable_advanceTime(100);
1935
- await advanceTimers(100);
1936
- // Start rendering
1937
- await waitFor(['Foo']);
1938
- // For some reason it took a long time to render Foo.
1939
- Scheduler.unstable_advanceTime(1250);
1940
- await advanceTimers(1250);
1941
- await waitForAll([
1942
- // A suspends
1943
- 'Suspend! [A]',
1944
- 'Loading...',
1945
- ]);
1946
- // We're now suspended and we haven't shown anything yet.
1947
- expect(ReactNoop).toMatchRenderedOutput(null);
1948
-
1949
- // Flush some of the time
1950
- Scheduler.unstable_advanceTime(450);
1951
- await advanceTimers(450);
1952
- // Because we've already been waiting for so long we can
1953
- // wait a bit longer. Still nothing...
1954
- await waitForAll([]);
1955
- expect(ReactNoop).toMatchRenderedOutput(null);
1956
-
1957
- // Eventually we'll show the fallback.
1958
- Scheduler.unstable_advanceTime(500);
1959
- await advanceTimers(500);
1960
- // No need to rerender.
1961
- await waitForAll([]);
1962
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
1963
- // Since this is a transition, we never fallback.
1964
- expect(ReactNoop).toMatchRenderedOutput(null);
1965
- } else {
1966
- expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
1967
- }
1968
-
1969
- // Flush the promise completely
1970
- await resolveText('A');
1971
- // Renders successfully
1972
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
1973
- // TODO: Why does this render Foo
1974
- await waitForAll(['Foo', 'A']);
1975
- } else {
1976
- await waitForAll(['A']);
1977
- }
1978
- expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
1979
- });
1980
-
1715
// @gate enableLegacyCache
1716
it('does not suspends if a fallback has been shown for a long time', async () => {
1717
function Foo() {
1822
);
1823
});
1824
2091
- // @gate enableLegacyCache
2092
- it('does not suspend for very long after a higher priority update', async () => {
2093
- function Foo({renderContent}) {
2094
- Scheduler.log('Foo');
2095
- return (
2096
- <Suspense fallback={<Text text="Loading..." />}>
2097
- {renderContent ? <AsyncText text="A" /> : null}
2098
- </Suspense>
2099
- );
2100
- }
2101
-
2102
- ReactNoop.render(<Foo />);
2103
- await waitForAll(['Foo']);
2104
-
2105
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
2106
- React.startTransition(() => {
2107
- ReactNoop.render(<Foo renderContent={true} />);
2108
- });
2109
- } else {
2110
- ReactNoop.render(<Foo renderContent={true} />);
2111
- }
2112
- await waitFor(['Foo']);
2113
-
2114
- // Advance some time.
2115
- Scheduler.unstable_advanceTime(100);
2116
- await advanceTimers(100);
2117
-
2118
- await waitForAll([
2119
- // A suspends
2120
- 'Suspend! [A]',
2121
- 'Loading...',
2122
- ]);
2123
-
2124
- // We're now suspended and we haven't shown anything yet.
2125
- expect(ReactNoop).toMatchRenderedOutput(null);
2126
-
2127
- // Flush some of the time
2128
- Scheduler.unstable_advanceTime(500);
2129
- jest.advanceTimersByTime(500);
2130
-
2131
- // We should have already shown the fallback.
2132
- // When we wrote this test, we inferred the start time of high priority
2133
- // updates as way earlier in the past. This test ensures that we don't
2134
- // use this assumption to add a very long JND.
2135
- await waitForAll([]);
2136
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
2137
- // Transitions never fallback.
2138
- expect(ReactNoop).toMatchRenderedOutput(null);
2139
- } else {
2140
- expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading..." />);
2141
- }
2142
- });
2143
-
1825
// TODO: flip to "warns" when this is implemented again.
1826
// @gate enableLegacyCache
1827
it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {
2180
}
2181
2182
await waitForAll(['Foo', 'A', 'Suspend! [B]', 'Loading B...']);
2502
- // Still suspended.
2503
- expect(ReactNoop).toMatchRenderedOutput(<span prop="A" />);
2504
-
2505
- // Flush to skip suspended time.
2506
- Scheduler.unstable_advanceTime(600);
2507
- await advanceTimers(600);
2183
2184
if (gate(flags => flags.enableSyncDefaultUpdates)) {
2185
// Transitions never fall back.
2245
}
2246
});
2247
2573
- // @gate enableLegacyCache
2574
- it('commits a suspended idle pri render within a reasonable time', async () => {
2575
- function Foo({renderContent}) {
2576
- return (
2577
- <Fragment>
2578
- <Suspense fallback={<Text text="Loading A..." />}>
2579
- {renderContent ? <AsyncText text="A" /> : null}
2580
- </Suspense>
2581
- </Fragment>
2582
- );
2583
- }
2584
-
2585
- ReactNoop.render(<Foo />);
2586
- await waitForAll([]);
2587
-
2588
- ReactNoop.render(<Foo renderContent={1} />);
2589
-
2590
- // Took a long time to render. This is to ensure we get a long suspense time.
2591
- // Could also use something like startTransition to simulate this.
2592
- Scheduler.unstable_advanceTime(1500);
2593
- await advanceTimers(1500);
2594
-
2595
- await waitForAll(['Suspend! [A]', 'Loading A...']);
2596
- // We're still suspended.
2597
- expect(ReactNoop).toMatchRenderedOutput(null);
2598
-
2599
- // Schedule an update at idle pri.
2600
- ReactNoop.idleUpdates(() => ReactNoop.render(<Foo renderContent={2} />));
2601
- // We won't even work on Idle priority.
2602
- await waitForAll([]);
2603
-
2604
- // We're still suspended.
2605
- expect(ReactNoop).toMatchRenderedOutput(null);
2606
-
2607
- // Advance time a little bit.
2608
- Scheduler.unstable_advanceTime(150);
2609
- await advanceTimers(150);
2610
-
2611
- // We should not have committed yet because we had a long suspense time.
2612
- expect(ReactNoop).toMatchRenderedOutput(null);
2613
-
2614
- // Flush to skip suspended time.
2615
- Scheduler.unstable_advanceTime(600);
2616
- await advanceTimers(600);
2617
-
2618
- expect(ReactNoop).toMatchRenderedOutput(<span prop="Loading A..." />);
2619
- });
2620
-
2248
describe('startTransition', () => {
2249
// @gate enableLegacyCache
2250
it('top level render', async () => {
2701
);
2702
});
2703
3077
- // TODO: This test is specifically about avoided commits that suspend for a
3078
- // JND. We may remove this behavior.
3079
- // @gate enableLegacyCache
3080
- it("suspended commit remains suspended even if there's another update at same expiration", async () => {
3081
- // Regression test
3082
- function App({text}) {
3083
- return (
3084
- <Suspense fallback="Loading...">
3085
- <AsyncText text={text} />
3086
- </Suspense>
3087
- );
3088
- }
3089
-
3090
- const root = ReactNoop.createRoot();
3091
- await act(() => {
3092
- root.render(<App text="Initial" />);
3093
- });
3094
- assertLog(['Suspend! [Initial]']);
3095
-
3096
- // Resolve initial render
3097
- await act(async () => {
3098
- await resolveText('Initial');
3099
- });
3100
- assertLog(['Initial']);
3101
- expect(root).toMatchRenderedOutput(<span prop="Initial" />);
3102
-
3103
- await act(async () => {
3104
- // Update. Since showing a fallback would hide content that's already
3105
- // visible, it should suspend for a JND without committing.
3106
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
3107
- React.startTransition(() => {
3108
- root.render(<App text="First update" />);
3109
- });
3110
- } else {
3111
- root.render(<App text="First update" />);
3112
- }
3113
- await waitForAll(['Suspend! [First update]']);
3114
-
3115
- // Should not display a fallback
3116
- expect(root).toMatchRenderedOutput(<span prop="Initial" />);
3117
-
3118
- // Update again. This should also suspend for a JND.
3119
- if (gate(flags => flags.enableSyncDefaultUpdates)) {
3120
- React.startTransition(() => {
3121
- root.render(<App text="Second update" />);
3122
- });
3123
- } else {
3124
- root.render(<App text="Second update" />);
3125
- }
3126
- await waitForAll(['Suspend! [Second update]']);
3127
-
3128
- // Should not display a fallback
3129
- expect(root).toMatchRenderedOutput(<span prop="Initial" />);
3130
- });
3131
- });
3132
-
2704
it('regression test: resets current "debug phase" after suspending', async () => {
2705
function App() {
2706
return (
2962
setText('C');
2963
});
2964
3394
- await waitForAll([
3395
- // First we attempt the high pri update. It suspends.
3396
- 'Suspend! [B]',
3397
- 'Loading...',
3398
- ]);
3399
-
3400
- // Commit the placeholder to unblock the Idle update.
3401
- await advanceTimers(250);
2965
+ // First we attempt the high pri update. It suspends.
2966
+ await waitForPaint(['Suspend! [B]', 'Loading...']);
2967
expect(root).toMatchRenderedOutput(
2968
<>
2969
<span hidden={true} prop="A" />
3530
]);
3531
expect(root).toMatchRenderedOutput(
3532
<>
3968
- <span prop="A" />
3533
+ <span hidden={true} prop="A" />
3534
+ <span prop="Loading..." />
3535
<span prop="B" />
3536
</>,
3537
);
3695
await act(async () => {
3696
setText('B');
3697
ReactNoop.idleUpdates(() => {
4132
- setText('B');
3698
+ setText('C');
3699
});
4134
- // Suspend the first update. The second update doesn't run because it has
4135
- // Idle priority.
4136
- await waitForAll(['Suspend! [B]', 'Loading...']);
3700
4138
- // Commit the fallback. Now we'll try working on Idle.
4139
- jest.runAllTimers();
3701
+ // Suspend the first update. This triggers an immediate fallback because
3702
+ // it wasn't wrapped in startTransition.
3703
+ await waitForPaint(['Suspend! [B]', 'Loading...']);
3704
+ expect(root).toMatchRenderedOutput(
3705
+ <>
3706
+ <span hidden={true} prop="A" />
3707
+ <span prop="Loading..." />
3708
+ </>,
3709
+ );
3710
4141
- // It also suspends.
4142
- await waitForAll(['Suspend! [B]']);
3711
+ // Once the fallback renders, proceed to the Idle update. This will
3712
+ // also suspend.
3713
+ await waitForAll(['Suspend! [C]']);
3714
});
3715
3716
+ // Finish loading B.
3717
await act(async () => {
3718
setText('B');
3719
await resolveText('B');
3720
});
3721
+ // We did not try to render the Idle update again because there have been no
3722
+ // additional updates since the last time it was attempted.
3723
assertLog(['B']);
3724
expect(root).toMatchRenderedOutput(<span prop="B" />);
3725
+
3726
+ // Finish loading C.
3727
+ await act(async () => {
3728
+ setText('C');
3729
+ await resolveText('C');
3730
+ });
3731
+ assertLog(['C']);
3732
+ expect(root).toMatchRenderedOutput(<span prop="C" />);
3733
});
3734
3735
// @gate enableLegacyCache