Add React.startTransition (#19696)
* Add React.startTransition * Export startTransition from index.js as well
Ricky committed
Aug 26, 2020 at 11:34 UTC
5564f2c95bb61b446f93dc5c519740bdb39e1989
7 files changed
+242
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+209
@@ -2514,6 +2514,215 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2514
});
2515
});
2516
2517
+ describe('delays transitions when using React.startTranistion', () => {
2518
+ // @gate experimental
2519
+ it('top level render', async () => {
2520
+ function App({page}) {
2521
+ return (
2522
+ <Suspense fallback={<Text text="Loading..." />}>
2523
+ <AsyncText text={page} ms={5000} />
2524
+ </Suspense>
2525
+ );
2526
+ }
2527
+
2528
+ // Initial render.
2529
+ React.unstable_startTransition(() => ReactNoop.render(<App page="A" />));
2530
+
2531
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2532
+ // Only a short time is needed to unsuspend the initial loading state.
2533
+ Scheduler.unstable_advanceTime(400);
2534
+ await advanceTimers(400);
2535
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2536
+
2537
+ // Later we load the data.
2538
+ Scheduler.unstable_advanceTime(5000);
2539
+ await advanceTimers(5000);
2540
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2541
+ expect(Scheduler).toFlushAndYield(['A']);
2542
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2543
+
2544
+ // Start transition.
2545
+ React.unstable_startTransition(() => ReactNoop.render(<App page="B" />));
2546
+
2547
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2548
+ Scheduler.unstable_advanceTime(2999);
2549
+ await advanceTimers(2999);
2550
+ // Since the timeout is infinite (or effectively infinite),
2551
+ // we have still not yet flushed the loading state.
2552
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2553
+
2554
+ // Later we load the data.
2555
+ Scheduler.unstable_advanceTime(3000);
2556
+ await advanceTimers(3000);
2557
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2558
+ expect(Scheduler).toFlushAndYield(['B']);
2559
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2560
+
2561
+ // Start a long (infinite) transition.
2562
+ React.unstable_startTransition(() => ReactNoop.render(<App page="C" />));
2563
+ expect(Scheduler).toFlushAndYield(['Suspend! [C]', 'Loading...']);
2564
+
2565
+ // Advance past the current (effectively) infinite timeout.
2566
+ // This is enforcing temporary behavior until it's truly infinite.
2567
+ Scheduler.unstable_advanceTime(100000);
2568
+ await advanceTimers(100000);
2569
+ expect(ReactNoop.getChildren()).toEqual([
2570
+ hiddenSpan('B'),
2571
+ span('Loading...'),
2572
+ ]);
2573
+ });
2574
+
2575
+ // @gate experimental
2576
+ it('hooks', async () => {
2577
+ let transitionToPage;
2578
+ function App() {
2579
+ const [page, setPage] = React.useState('none');
2580
+ transitionToPage = setPage;
2581
+ if (page === 'none') {
2582
+ return null;
2583
+ }
2584
+ return (
2585
+ <Suspense fallback={<Text text="Loading..." />}>
2586
+ <AsyncText text={page} ms={5000} />
2587
+ </Suspense>
2588
+ );
2589
+ }
2590
+
2591
+ ReactNoop.render(<App />);
2592
+ expect(Scheduler).toFlushAndYield([]);
2593
+
2594
+ // Initial render.
2595
+ await ReactNoop.act(async () => {
2596
+ React.unstable_startTransition(() => transitionToPage('A'));
2597
+
2598
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2599
+ // Only a short time is needed to unsuspend the initial loading state.
2600
+ Scheduler.unstable_advanceTime(400);
2601
+ await advanceTimers(400);
2602
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2603
+ });
2604
+
2605
+ // Later we load the data.
2606
+ Scheduler.unstable_advanceTime(5000);
2607
+ await advanceTimers(5000);
2608
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2609
+ expect(Scheduler).toFlushAndYield(['A']);
2610
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2611
+
2612
+ // Start transition.
2613
+ await ReactNoop.act(async () => {
2614
+ React.unstable_startTransition(() => transitionToPage('B'));
2615
+
2616
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2617
+
2618
+ Scheduler.unstable_advanceTime(2999);
2619
+ await advanceTimers(2999);
2620
+ // Since the timeout is infinite (or effectively infinite),
2621
+ // we have still not yet flushed the loading state.
2622
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2623
+ });
2624
+
2625
+ // Later we load the data.
2626
+ Scheduler.unstable_advanceTime(3000);
2627
+ await advanceTimers(3000);
2628
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2629
+ expect(Scheduler).toFlushAndYield(['B']);
2630
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2631
+
2632
+ // Start a long (infinite) transition.
2633
+ await ReactNoop.act(async () => {
2634
+ React.unstable_startTransition(() => transitionToPage('C'));
2635
+
2636
+ expect(Scheduler).toFlushAndYield(['Suspend! [C]', 'Loading...']);
2637
+
2638
+ // Advance past the current effectively infinite timeout.
2639
+ // This is enforcing temporary behavior until it's truly infinite.
2640
+ Scheduler.unstable_advanceTime(100000);
2641
+ await advanceTimers(100000);
2642
+ expect(ReactNoop.getChildren()).toEqual([
2643
+ hiddenSpan('B'),
2644
+ span('Loading...'),
2645
+ ]);
2646
+ });
2647
+ });
2648
+
2649
+ // @gate experimental
2650
+ it('classes', async () => {
2651
+ let transitionToPage;
2652
+ class App extends React.Component {
2653
+ state = {page: 'none'};
2654
+ render() {
2655
+ transitionToPage = page => this.setState({page});
2656
+ const page = this.state.page;
2657
+ if (page === 'none') {
2658
+ return null;
2659
+ }
2660
+ return (
2661
+ <Suspense fallback={<Text text="Loading..." />}>
2662
+ <AsyncText text={page} ms={5000} />
2663
+ </Suspense>
2664
+ );
2665
+ }
2666
+ }
2667
+
2668
+ ReactNoop.render(<App />);
2669
+ expect(Scheduler).toFlushAndYield([]);
2670
+
2671
+ // Initial render.
2672
+ await ReactNoop.act(async () => {
2673
+ React.unstable_startTransition(() => transitionToPage('A'));
2674
+
2675
+ expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
2676
+ // Only a short time is needed to unsuspend the initial loading state.
2677
+ Scheduler.unstable_advanceTime(400);
2678
+ await advanceTimers(400);
2679
+ expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
2680
+ });
2681
+
2682
+ // Later we load the data.
2683
+ Scheduler.unstable_advanceTime(5000);
2684
+ await advanceTimers(5000);
2685
+ expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
2686
+ expect(Scheduler).toFlushAndYield(['A']);
2687
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2688
+
2689
+ // Start transition.
2690
+ await ReactNoop.act(async () => {
2691
+ React.unstable_startTransition(() => transitionToPage('B'));
2692
+
2693
+ expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
2694
+ Scheduler.unstable_advanceTime(2999);
2695
+ await advanceTimers(2999);
2696
+ // Since the timeout is infinite (or effectively infinite),
2697
+ // we have still not yet flushed the loading state.
2698
+ expect(ReactNoop.getChildren()).toEqual([span('A')]);
2699
+ });
2700
+
2701
+ // Later we load the data.
2702
+ Scheduler.unstable_advanceTime(3000);
2703
+ await advanceTimers(3000);
2704
+ expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
2705
+ expect(Scheduler).toFlushAndYield(['B']);
2706
+ expect(ReactNoop.getChildren()).toEqual([span('B')]);
2707
+
2708
+ // Start a long (infinite) transition.
2709
+ await ReactNoop.act(async () => {
2710
+ React.unstable_startTransition(() => transitionToPage('C'));
2711
+
2712
+ expect(Scheduler).toFlushAndYield(['Suspend! [C]', 'Loading...']);
2713
+
2714
+ // Advance past the current effectively infinite timeout.
2715
+ // This is enforcing temporary behavior until it's truly infinite.
2716
+ Scheduler.unstable_advanceTime(100000);
2717
+ await advanceTimers(100000);
2718
+ expect(ReactNoop.getChildren()).toEqual([
2719
+ hiddenSpan('B'),
2720
+ span('Loading...'),
2721
+ ]);
2722
+ });
2723
+ });
2724
+ });
2725
+
2726
// @gate experimental
2727
it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
2728
function App({page}) {
packages/react/index.classic.fb.js
+2
@@ -46,6 +46,8 @@ export {
46
useTransition as unstable_useTransition,
47
useDeferredValue,
48
useDeferredValue as unstable_useDeferredValue,
49
+ startTransition,
50
+ startTransition as unstable_startTransition,
51
SuspenseList,
52
SuspenseList as unstable_SuspenseList,
53
unstable_withSuspenseConfig,
packages/react/index.experimental.js
+1
@@ -42,6 +42,7 @@ export {
42
// exposeConcurrentModeAPIs
43
useTransition as unstable_useTransition,
44
useDeferredValue as unstable_useDeferredValue,
45
+ startTransition as unstable_startTransition,
46
SuspenseList as unstable_SuspenseList,
47
unstable_withSuspenseConfig,
48
// enableBlocksAPI
packages/react/index.js
+2
@@ -72,6 +72,8 @@ export {
72
createFactory,
73
useTransition,
74
useTransition as unstable_useTransition,
75
+ startTransition,
76
+ startTransition as unstable_startTransition,
77
useDeferredValue,
78
useDeferredValue as unstable_useDeferredValue,
79
SuspenseList,
packages/react/index.modern.fb.js
+2
@@ -45,6 +45,8 @@ export {
45
useTransition as unstable_useTransition,
46
useDeferredValue,
47
useDeferredValue as unstable_useDeferredValue,
48
+ startTransition,
49
+ startTransition as unstable_startTransition,
50
SuspenseList,
51
SuspenseList as unstable_SuspenseList,
52
unstable_withSuspenseConfig,
packages/react/src/React.js
+2
@@ -58,6 +58,7 @@ import {
58
import {createMutableSource} from './ReactMutableSource';
59
import ReactSharedInternals from './ReactSharedInternals';
60
import {createFundamental} from './ReactFundamental';
61
+import {startTransition} from './ReactStartTransition';
62
63
// TODO: Move this branching into the other module instead and just re-export.
64
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
@@ -107,6 +108,7 @@ export {
108
createFactory,
109
// Concurrent Mode
110
useTransition,
111
+ startTransition,
112
useDeferredValue,
113
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
114
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
packages/react/src/ReactStartTransition.js
new
+24
@@ -0,0 +1,24 @@
1
+/**
2
+ * Copyright (c) Facebook, Inc. and its affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';
11
+
12
+// Default to an arbitrarily large timeout. Effectively, this is infinite. The
13
+// eventual goal is to never timeout when refreshing already visible content.
14
+const IndefiniteTimeoutConfig = {timeoutMs: 100000};
15
+
16
+export function startTransition(scope: () => void) {
17
+ const previousConfig = ReactCurrentBatchConfig.suspense;
18
+ ReactCurrentBatchConfig.suspense = IndefiniteTimeoutConfig;
19
+ try {
20
+ scope();
21
+ } finally {
22
+ ReactCurrentBatchConfig.suspense = previousConfig;
23
+ }
24
+}