Warn if addTransitionType is called when there are no pending Actions (#32793)

Stacked on #32792.

It's tricky to associate a specific `addTransitionType` call to a
specific `startTransition` call because we don't have `AsyncContext` in
browsers yet. However, we can keep track if there are any async
transitions running at all, and if not, warn. This should cover most
cases.

This also errors when inside a React render which might be a legit way
to associate a Transition Type to a specific render (e.g. based on props
changing) but we want to be a more conservative about allowing that yet.
If we wanted to support calling it in render, we might want to set which
Transition object is currently rendering but it's still tricky if the
render has `async function` components. So it might at least be
restricted to sync components (like Hooks).
This commit is contained in:
Sebastian Markbåge
2025-04-01 12:10:10 -04:00
committed by GitHub
parent c1b6c61b1c
commit 6b659aba2f
4 changed files with 50 additions and 0 deletions
+16
View File
@@ -2219,6 +2219,11 @@ function handleActionReturnValue<S, P>(
typeof returnValue.then === 'function'
) {
const thenable = ((returnValue: any): Thenable<Awaited<S>>);
if (__DEV__) {
// Keep track of the number of async transitions still running so we can warn.
ReactSharedInternals.asyncTransitions++;
thenable.then(releaseAsyncTransition, releaseAsyncTransition);
}
// Attach a listener to read the return state of the action. As soon as
// this resolves, we can run the next action in the sequence.
thenable.then(
@@ -3026,6 +3031,12 @@ function updateDeferredValueImpl<T>(
}
}
function releaseAsyncTransition() {
if (__DEV__) {
ReactSharedInternals.asyncTransitions--;
}
}
function startTransition<S>(
fiber: Fiber,
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
@@ -3083,6 +3094,11 @@ function startTransition<S>(
typeof returnValue.then === 'function'
) {
const thenable = ((returnValue: any): Thenable<mixed>);
if (__DEV__) {
// Keep track of the number of async transitions still running so we can warn.
ReactSharedInternals.asyncTransitions++;
thenable.then(releaseAsyncTransition, releaseAsyncTransition);
}
// Create a thenable that resolves to `finishedState` once the async
// action has completed.
const thenableForFinishedState = chainThenableValue(
@@ -39,6 +39,9 @@ export type SharedStateClient = {
// ReactCurrentActQueue
actQueue: null | Array<RendererTask>,
// When zero this means we're outside an async startTransition.
asyncTransitions: number,
// Used to reproduce behavior of `batchedUpdates` in legacy mode.
isBatchingLegacy: boolean,
didScheduleLegacyUpdate: boolean,
@@ -75,6 +78,7 @@ if (enableViewTransition) {
if (__DEV__) {
ReactSharedInternals.actQueue = null;
ReactSharedInternals.asyncTransitions = 0;
ReactSharedInternals.isBatchingLegacy = false;
ReactSharedInternals.didScheduleLegacyUpdate = false;
ReactSharedInternals.didUsePromise = false;
@@ -37,6 +37,12 @@ export type Transition = {
...
};
function releaseAsyncTransition() {
if (__DEV__) {
ReactSharedInternals.asyncTransitions--;
}
}
export function startTransition(
scope: () => void,
options?: StartTransitionOptions,
@@ -67,6 +73,11 @@ export function startTransition(
returnValue !== null &&
typeof returnValue.then === 'function'
) {
if (__DEV__) {
// Keep track of the number of async transitions still running so we can warn.
ReactSharedInternals.asyncTransitions++;
returnValue.then(releaseAsyncTransition, releaseAsyncTransition);
}
returnValue.then(noop, reportGlobalError);
}
} catch (error) {
+19
View File
@@ -45,6 +45,25 @@ export function addTransitionType(type: string): void {
pendingTransitionTypes = pendingGestureTransitionTypes = [];
}
} else {
if (__DEV__) {
if (
ReactSharedInternals.T === null &&
ReactSharedInternals.asyncTransitions === 0
) {
if (enableGestureTransition) {
console.error(
'addTransitionType can only be called inside a `startTransition()` ' +
'or `startGestureTransition()` callback. ' +
'It must be associated with a specific Transition.',
);
} else {
console.error(
'addTransitionType can only be called inside a `startTransition()` ' +
'callback. It must be associated with a specific Transition.',
);
}
}
}
// Otherwise we're either inside a synchronous startTransition
// or in the async gap of one, which we track globally.
pendingTransitionTypes = ReactSharedInternals.V;