mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Add React.startTransition (#19696)
* Add React.startTransition * Export startTransition from index.js as well
This commit is contained in:
+209
@@ -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}) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -72,6 +72,8 @@ export {
|
||||
createFactory,
|
||||
useTransition,
|
||||
useTransition as unstable_useTransition,
|
||||
startTransition,
|
||||
startTransition as unstable_startTransition,
|
||||
useDeferredValue,
|
||||
useDeferredValue as unstable_useDeferredValue,
|
||||
SuspenseList,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -107,6 +108,7 @@ export {
|
||||
createFactory,
|
||||
// Concurrent Mode
|
||||
useTransition,
|
||||
startTransition,
|
||||
useDeferredValue,
|
||||
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
|
||||
REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user