Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add React.startTransition #19696

Merged
merged 2 commits into from Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
1 change: 1 addition & 0 deletions packages/react/index.js
Expand Up @@ -72,6 +72,7 @@ export {
createFactory,
useTransition,
useTransition as unstable_useTransition,
startTransition as unstable_startTransition,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this also need an unprefixed export like others?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, it doesn't need it but yes it should have it.

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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can we rename this to .currentTransition please? To match the convention of the other ref-like things

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it was already like that so it's fine. We can rethink the internal data structure later.

try {
scope();
} finally {
ReactCurrentBatchConfig.suspense = previousConfig;
}
}