@samitouri / QOS-React-2 / commits / 5564f2c95b

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 +}