Skip to content

Commit

Permalink
Add React.startTransition (facebook#19696)
Browse files Browse the repository at this point in the history
* Add React.startTransition

* Export startTransition from index.js as well
  • Loading branch information
rickhanlonii authored and koto committed Jun 15, 2021
1 parent 4223ad4 commit d4d74dc
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 0 deletions.
Expand Up @@ -2514,6 +2514,215 @@ describe('ReactSuspenseWithNoopRenderer', () => {
});
});

describe('delays transitions when using React.startTranistion', () => {
// @gate experimental
it('top level render', async () => {
function App({page}) {
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={page} ms={5000} />
</Suspense>
);
}

// Initial render.
React.unstable_startTransition(() => ReactNoop.render(<App page="A" />));

expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
// Only a short time is needed to unsuspend the initial loading state.
Scheduler.unstable_advanceTime(400);
await advanceTimers(400);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);

// Later we load the data.
Scheduler.unstable_advanceTime(5000);
await advanceTimers(5000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);

// Start transition.
React.unstable_startTransition(() => ReactNoop.render(<App page="B" />));

expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
// we have still not yet flushed the loading state.
expect(ReactNoop.getChildren()).toEqual([span('A')]);

// Later we load the data.
Scheduler.unstable_advanceTime(3000);
await advanceTimers(3000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);

// Start a long (infinite) transition.
React.unstable_startTransition(() => ReactNoop.render(<App page="C" />));
expect(Scheduler).toFlushAndYield(['Suspend! [C]', 'Loading...']);

// Advance past the current (effectively) infinite timeout.
// This is enforcing temporary behavior until it's truly infinite.
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
expect(ReactNoop.getChildren()).toEqual([
hiddenSpan('B'),
span('Loading...'),
]);
});

// @gate experimental
it('hooks', async () => {
let transitionToPage;
function App() {
const [page, setPage] = React.useState('none');
transitionToPage = setPage;
if (page === 'none') {
return null;
}
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={page} ms={5000} />
</Suspense>
);
}

ReactNoop.render(<App />);
expect(Scheduler).toFlushAndYield([]);

// Initial render.
await ReactNoop.act(async () => {
React.unstable_startTransition(() => transitionToPage('A'));

expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
// Only a short time is needed to unsuspend the initial loading state.
Scheduler.unstable_advanceTime(400);
await advanceTimers(400);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
});

// Later we load the data.
Scheduler.unstable_advanceTime(5000);
await advanceTimers(5000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);

// Start transition.
await ReactNoop.act(async () => {
React.unstable_startTransition(() => transitionToPage('B'));

expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);

Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
// we have still not yet flushed the loading state.
expect(ReactNoop.getChildren()).toEqual([span('A')]);
});

// Later we load the data.
Scheduler.unstable_advanceTime(3000);
await advanceTimers(3000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);

// Start a long (infinite) transition.
await ReactNoop.act(async () => {
React.unstable_startTransition(() => transitionToPage('C'));

expect(Scheduler).toFlushAndYield(['Suspend! [C]', 'Loading...']);

// Advance past the current effectively infinite timeout.
// This is enforcing temporary behavior until it's truly infinite.
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
expect(ReactNoop.getChildren()).toEqual([
hiddenSpan('B'),
span('Loading...'),
]);
});
});

// @gate experimental
it('classes', async () => {
let transitionToPage;
class App extends React.Component {
state = {page: 'none'};
render() {
transitionToPage = page => this.setState({page});
const page = this.state.page;
if (page === 'none') {
return null;
}
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={page} ms={5000} />
</Suspense>
);
}
}

ReactNoop.render(<App />);
expect(Scheduler).toFlushAndYield([]);

// Initial render.
await ReactNoop.act(async () => {
React.unstable_startTransition(() => transitionToPage('A'));

expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
// Only a short time is needed to unsuspend the initial loading state.
Scheduler.unstable_advanceTime(400);
await advanceTimers(400);
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
});

// Later we load the data.
Scheduler.unstable_advanceTime(5000);
await advanceTimers(5000);
expect(Scheduler).toHaveYielded(['Promise resolved [A]']);
expect(Scheduler).toFlushAndYield(['A']);
expect(ReactNoop.getChildren()).toEqual([span('A')]);

// Start transition.
await ReactNoop.act(async () => {
React.unstable_startTransition(() => transitionToPage('B'));

expect(Scheduler).toFlushAndYield(['Suspend! [B]', 'Loading...']);
Scheduler.unstable_advanceTime(2999);
await advanceTimers(2999);
// Since the timeout is infinite (or effectively infinite),
// we have still not yet flushed the loading state.
expect(ReactNoop.getChildren()).toEqual([span('A')]);
});

// Later we load the data.
Scheduler.unstable_advanceTime(3000);
await advanceTimers(3000);
expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop.getChildren()).toEqual([span('B')]);

// Start a long (infinite) transition.
await ReactNoop.act(async () => {
React.unstable_startTransition(() => transitionToPage('C'));

expect(Scheduler).toFlushAndYield(['Suspend! [C]', 'Loading...']);

// Advance past the current effectively infinite timeout.
// This is enforcing temporary behavior until it's truly infinite.
Scheduler.unstable_advanceTime(100000);
await advanceTimers(100000);
expect(ReactNoop.getChildren()).toEqual([
hiddenSpan('B'),
span('Loading...'),
]);
});
});
});

// @gate experimental
it('disables suspense config when nothing is passed to withSuspenseConfig', async () => {
function App({page}) {
Expand Down
2 changes: 2 additions & 0 deletions packages/react/index.classic.fb.js
Expand Up @@ -46,6 +46,8 @@ export {
useTransition as unstable_useTransition,
useDeferredValue,
useDeferredValue as unstable_useDeferredValue,
startTransition,
startTransition as unstable_startTransition,
SuspenseList,
SuspenseList as unstable_SuspenseList,
unstable_withSuspenseConfig,
Expand Down
1 change: 1 addition & 0 deletions packages/react/index.experimental.js
Expand Up @@ -42,6 +42,7 @@ export {
// exposeConcurrentModeAPIs
useTransition as unstable_useTransition,
useDeferredValue as unstable_useDeferredValue,
startTransition as unstable_startTransition,
SuspenseList as unstable_SuspenseList,
unstable_withSuspenseConfig,
// enableBlocksAPI
Expand Down
2 changes: 2 additions & 0 deletions packages/react/index.js
Expand Up @@ -72,6 +72,8 @@ export {
createFactory,
useTransition,
useTransition as unstable_useTransition,
startTransition,
startTransition as unstable_startTransition,
useDeferredValue,
useDeferredValue as unstable_useDeferredValue,
SuspenseList,
Expand Down
2 changes: 2 additions & 0 deletions packages/react/index.modern.fb.js
Expand Up @@ -45,6 +45,8 @@ export {
useTransition as unstable_useTransition,
useDeferredValue,
useDeferredValue as unstable_useDeferredValue,
startTransition,
startTransition as unstable_startTransition,
SuspenseList,
SuspenseList as unstable_SuspenseList,
unstable_withSuspenseConfig,
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/React.js
Expand Up @@ -58,6 +58,7 @@ import {
import {createMutableSource} from './ReactMutableSource';
import ReactSharedInternals from './ReactSharedInternals';
import {createFundamental} from './ReactFundamental';
import {startTransition} from './ReactStartTransition';

// TODO: Move this branching into the other module instead and just re-export.
const createElement = __DEV__ ? createElementWithValidation : createElementProd;
Expand Down Expand Up @@ -107,6 +108,7 @@ export {
createFactory,
// Concurrent Mode
useTransition,
startTransition,
useDeferredValue,
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
Expand Down
24 changes: 24 additions & 0 deletions packages/react/src/ReactStartTransition.js
@@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import ReactCurrentBatchConfig from './ReactCurrentBatchConfig';

// Default to an arbitrarily large timeout. Effectively, this is infinite. The
// eventual goal is to never timeout when refreshing already visible content.
const IndefiniteTimeoutConfig = {timeoutMs: 100000};

export function startTransition(scope: () => void) {
const previousConfig = ReactCurrentBatchConfig.suspense;
ReactCurrentBatchConfig.suspense = IndefiniteTimeoutConfig;
try {
scope();
} finally {
ReactCurrentBatchConfig.suspense = previousConfig;
}
}

0 comments on commit d4d74dc

Please sign in to comment.