mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
cd2b79dedd
Implements initial (client-only) support for async actions behind a flag. This is an experimental feature and the design isn't completely finalized but we're getting closer. It will be layered alongside other features we're working on, so it may not feel complete when considered in isolation. The basic description is you can pass an async function to `startTransition` and all the transition updates that are scheduled inside that async function will be grouped together. The `isPending` flag will be set to true immediately, and only set back to false once the async action has completed (as well as all the updates that it triggers). The ideal behavior would be that all updates spawned by the async action are automatically inferred and grouped together; however, doing this properly requires the upcoming (stage 2) Async Context API, which is not yet implemented by browsers. In the meantime, we will fake this by grouping together all transition updates that occur until the async function has terminated. This can lead to overgrouping between unrelated actions, which is not wrong per se, just not ideal. If the `useTransition` hook is removed from the UI before an async action has completed — for example, if the user navigates to a new page — subsequent transitions will no longer be grouped with together with that action. Another consequence of the lack of Async Context is that if you call `setState` inside an action but after an `await`, it must be wrapped in `startTransition` in order to be grouped properly. If we didn't require this, then there would be no way to distinguish action updates from urgent updates caused by user input, too. This is an unfortunate footgun but we can likely detect the most common mistakes using a lint rule. Once Async Context lands in browsers, we can start warning in dev if we detect an update that hasn't been wrapped in `startTransition`. Then, longer term, once the feature is ubiquitous, we can rely on it for real and allow you to call `setState` without the additional wrapper. Things that are _not_ yet implemented in this PR, but will be added as follow ups: - Support for non-hook form of `startTransition` - Canceling the async action scope if the `useTransition` hook is deleted from the UI - Anything related to server actions
131 lines
3.6 KiB
JavaScript
131 lines
3.6 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 type {Wakeable} from 'shared/ReactTypes';
|
|
import type {Lane} from './ReactFiberLane';
|
|
import {requestTransitionLane} from './ReactFiberRootScheduler';
|
|
|
|
interface AsyncActionImpl {
|
|
lane: Lane;
|
|
listeners: Array<(false) => mixed>;
|
|
count: number;
|
|
then(
|
|
onFulfill: (value: boolean) => mixed,
|
|
onReject: (error: mixed) => mixed,
|
|
): void;
|
|
}
|
|
|
|
interface PendingAsyncAction extends AsyncActionImpl {
|
|
status: 'pending';
|
|
}
|
|
|
|
interface FulfilledAsyncAction extends AsyncActionImpl {
|
|
status: 'fulfilled';
|
|
value: boolean;
|
|
}
|
|
|
|
interface RejectedAsyncAction extends AsyncActionImpl {
|
|
status: 'rejected';
|
|
reason: mixed;
|
|
}
|
|
|
|
type AsyncAction =
|
|
| PendingAsyncAction
|
|
| FulfilledAsyncAction
|
|
| RejectedAsyncAction;
|
|
|
|
let currentAsyncAction: AsyncAction | null = null;
|
|
|
|
export function requestAsyncActionContext(
|
|
actionReturnValue: mixed,
|
|
): AsyncAction | false {
|
|
if (
|
|
actionReturnValue !== null &&
|
|
typeof actionReturnValue === 'object' &&
|
|
typeof actionReturnValue.then === 'function'
|
|
) {
|
|
// This is an async action.
|
|
//
|
|
// Return a thenable that resolves once the action scope (i.e. the async
|
|
// function passed to startTransition) has finished running. The fulfilled
|
|
// value is `false` to represent that the action is not pending.
|
|
const thenable: Wakeable = (actionReturnValue: any);
|
|
if (currentAsyncAction === null) {
|
|
// There's no outer async action scope. Create a new one.
|
|
const asyncAction: AsyncAction = {
|
|
lane: requestTransitionLane(),
|
|
listeners: [],
|
|
count: 0,
|
|
status: 'pending',
|
|
value: false,
|
|
reason: undefined,
|
|
then(resolve: boolean => mixed) {
|
|
asyncAction.listeners.push(resolve);
|
|
},
|
|
};
|
|
attachPingListeners(thenable, asyncAction);
|
|
currentAsyncAction = asyncAction;
|
|
return asyncAction;
|
|
} else {
|
|
// Inherit the outer scope.
|
|
const asyncAction: AsyncAction = (currentAsyncAction: any);
|
|
attachPingListeners(thenable, asyncAction);
|
|
return asyncAction;
|
|
}
|
|
} else {
|
|
// This is not an async action, but it may be part of an outer async action.
|
|
if (currentAsyncAction === null) {
|
|
// There's no outer async action scope.
|
|
return false;
|
|
} else {
|
|
// Inherit the outer scope.
|
|
return currentAsyncAction;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function peekAsyncActionContext(): AsyncAction | null {
|
|
return currentAsyncAction;
|
|
}
|
|
|
|
function attachPingListeners(thenable: Wakeable, asyncAction: AsyncAction) {
|
|
asyncAction.count++;
|
|
thenable.then(
|
|
() => {
|
|
if (--asyncAction.count === 0) {
|
|
const fulfilledAsyncAction: FulfilledAsyncAction = (asyncAction: any);
|
|
fulfilledAsyncAction.status = 'fulfilled';
|
|
completeAsyncActionScope(asyncAction);
|
|
}
|
|
},
|
|
(error: mixed) => {
|
|
if (--asyncAction.count === 0) {
|
|
const rejectedAsyncAction: RejectedAsyncAction = (asyncAction: any);
|
|
rejectedAsyncAction.status = 'rejected';
|
|
rejectedAsyncAction.reason = error;
|
|
completeAsyncActionScope(asyncAction);
|
|
}
|
|
},
|
|
);
|
|
return asyncAction;
|
|
}
|
|
|
|
function completeAsyncActionScope(action: AsyncAction) {
|
|
if (currentAsyncAction === action) {
|
|
currentAsyncAction = null;
|
|
}
|
|
|
|
const listeners = action.listeners;
|
|
action.listeners = [];
|
|
for (let i = 0; i < listeners.length; i++) {
|
|
const listener = listeners[i];
|
|
listener(false);
|
|
}
|
|
}
|