Skip to content

Commit 5564f2c

Browse files
authored
Add React.startTransition (#19696)
* Add React.startTransition * Export startTransition from index.js as well
1 parent c4e0768 commit 5564f2c

File tree

7 files changed

+242
-0
lines changed

7 files changed

+242
-0
lines changed

packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js

+209
Original file line numberDiff line numberDiff line change
@@ -2514,6 +2514,215 @@ describe('ReactSuspenseWithNoopRenderer', () => {
25142514
});
25152515
});
25162516

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+
25172726
// @gate experimental
25182727
it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
25192728
function App({page}) {

packages/react/index.classic.fb.js

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export {
4646
useTransition as unstable_useTransition,
4747
useDeferredValue,
4848
useDeferredValue as unstable_useDeferredValue,
49+
startTransition,
50+
startTransition as unstable_startTransition,
4951
SuspenseList,
5052
SuspenseList as unstable_SuspenseList,
5153
unstable_withSuspenseConfig,

packages/react/index.experimental.js

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export {
4242
// exposeConcurrentModeAPIs
4343
useTransition as unstable_useTransition,
4444
useDeferredValue as unstable_useDeferredValue,
45+
startTransition as unstable_startTransition,
4546
SuspenseList as unstable_SuspenseList,
4647
unstable_withSuspenseConfig,
4748
// enableBlocksAPI

packages/react/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export {
7272
createFactory,
7373
useTransition,
7474
useTransition as unstable_useTransition,
75+
startTransition,
76+
startTransition as unstable_startTransition,
7577
useDeferredValue,
7678
useDeferredValue as unstable_useDeferredValue,
7779
SuspenseList,

packages/react/index.modern.fb.js

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export {
4545
useTransition as unstable_useTransition,
4646
useDeferredValue,
4747
useDeferredValue as unstable_useDeferredValue,
48+
startTransition,
49+
startTransition as unstable_startTransition,
4850
SuspenseList,
4951
SuspenseList as unstable_SuspenseList,
5052
unstable_withSuspenseConfig,

packages/react/src/React.js

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
import {createMutableSource} from './ReactMutableSource';
5959
import ReactSharedInternals from './ReactSharedInternals';
6060
import {createFundamental} from './ReactFundamental';
61+
import {startTransition} from './ReactStartTransition';
6162

6263
// TODO: Move this branching into the other module instead and just re-export.
6364
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
@@ -107,6 +108,7 @@ export {
107108
createFactory,
108109
// Concurrent Mode
109110
useTransition,
111+
startTransition,
110112
useDeferredValue,
111113
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
112114
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
+24
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)