Skip to content

Commit deca965

Browse files
authored
Warn if addTransitionType is called when there are no pending Actions (facebook#32793)
Stacked on facebook#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).
1 parent 0b1a9e9 commit deca965

File tree

4 files changed

+50
-0
lines changed

4 files changed

+50
-0
lines changed

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,11 @@ function handleActionReturnValue<S, P>(
22192219
typeof returnValue.then === 'function'
22202220
) {
22212221
const thenable = ((returnValue: any): Thenable<Awaited<S>>);
2222+
if (__DEV__) {
2223+
// Keep track of the number of async transitions still running so we can warn.
2224+
ReactSharedInternals.asyncTransitions++;
2225+
thenable.then(releaseAsyncTransition, releaseAsyncTransition);
2226+
}
22222227
// Attach a listener to read the return state of the action. As soon as
22232228
// this resolves, we can run the next action in the sequence.
22242229
thenable.then(
@@ -3026,6 +3031,12 @@ function updateDeferredValueImpl<T>(
30263031
}
30273032
}
30283033

3034+
function releaseAsyncTransition() {
3035+
if (__DEV__) {
3036+
ReactSharedInternals.asyncTransitions--;
3037+
}
3038+
}
3039+
30293040
function startTransition<S>(
30303041
fiber: Fiber,
30313042
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
@@ -3083,6 +3094,11 @@ function startTransition<S>(
30833094
typeof returnValue.then === 'function'
30843095
) {
30853096
const thenable = ((returnValue: any): Thenable<mixed>);
3097+
if (__DEV__) {
3098+
// Keep track of the number of async transitions still running so we can warn.
3099+
ReactSharedInternals.asyncTransitions++;
3100+
thenable.then(releaseAsyncTransition, releaseAsyncTransition);
3101+
}
30863102
// Create a thenable that resolves to `finishedState` once the async
30873103
// action has completed.
30883104
const thenableForFinishedState = chainThenableValue(

packages/react/src/ReactSharedInternalsClient.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export type SharedStateClient = {
3939
// ReactCurrentActQueue
4040
actQueue: null | Array<RendererTask>,
4141

42+
// When zero this means we're outside an async startTransition.
43+
asyncTransitions: number,
44+
4245
// Used to reproduce behavior of `batchedUpdates` in legacy mode.
4346
isBatchingLegacy: boolean,
4447
didScheduleLegacyUpdate: boolean,
@@ -75,6 +78,7 @@ if (enableViewTransition) {
7578

7679
if (__DEV__) {
7780
ReactSharedInternals.actQueue = null;
81+
ReactSharedInternals.asyncTransitions = 0;
7882
ReactSharedInternals.isBatchingLegacy = false;
7983
ReactSharedInternals.didScheduleLegacyUpdate = false;
8084
ReactSharedInternals.didUsePromise = false;

packages/react/src/ReactStartTransition.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export type Transition = {
3737
...
3838
};
3939

40+
function releaseAsyncTransition() {
41+
if (__DEV__) {
42+
ReactSharedInternals.asyncTransitions--;
43+
}
44+
}
45+
4046
export function startTransition(
4147
scope: () => void,
4248
options?: StartTransitionOptions,
@@ -67,6 +73,11 @@ export function startTransition(
6773
returnValue !== null &&
6874
typeof returnValue.then === 'function'
6975
) {
76+
if (__DEV__) {
77+
// Keep track of the number of async transitions still running so we can warn.
78+
ReactSharedInternals.asyncTransitions++;
79+
returnValue.then(releaseAsyncTransition, releaseAsyncTransition);
80+
}
7081
returnValue.then(noop, reportGlobalError);
7182
}
7283
} catch (error) {

packages/react/src/ReactTransitionType.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,25 @@ export function addTransitionType(type: string): void {
4545
pendingTransitionTypes = pendingGestureTransitionTypes = [];
4646
}
4747
} else {
48+
if (__DEV__) {
49+
if (
50+
ReactSharedInternals.T === null &&
51+
ReactSharedInternals.asyncTransitions === 0
52+
) {
53+
if (enableGestureTransition) {
54+
console.error(
55+
'addTransitionType can only be called inside a `startTransition()` ' +
56+
'or `startGestureTransition()` callback. ' +
57+
'It must be associated with a specific Transition.',
58+
);
59+
} else {
60+
console.error(
61+
'addTransitionType can only be called inside a `startTransition()` ' +
62+
'callback. It must be associated with a specific Transition.',
63+
);
64+
}
65+
}
66+
}
4867
// Otherwise we're either inside a synchronous startTransition
4968
// or in the async gap of one, which we track globally.
5069
pendingTransitionTypes = ReactSharedInternals.V;

0 commit comments

Comments
 (0)