diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index c8136f8fa03b2..c5ec0a16ef09a 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -2219,6 +2219,11 @@ function handleActionReturnValue( typeof returnValue.then === 'function' ) { const thenable = ((returnValue: any): Thenable>); + 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( } } +function releaseAsyncTransition() { + if (__DEV__) { + ReactSharedInternals.asyncTransitions--; + } +} + function startTransition( fiber: Fiber, queue: UpdateQueue, BasicStateAction>>, @@ -3083,6 +3094,11 @@ function startTransition( typeof returnValue.then === 'function' ) { const thenable = ((returnValue: any): Thenable); + 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( diff --git a/packages/react/src/ReactSharedInternalsClient.js b/packages/react/src/ReactSharedInternalsClient.js index f1bc3463644a8..725721c7a5d8a 100644 --- a/packages/react/src/ReactSharedInternalsClient.js +++ b/packages/react/src/ReactSharedInternalsClient.js @@ -39,6 +39,9 @@ export type SharedStateClient = { // ReactCurrentActQueue actQueue: null | Array, + // 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; diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index f8a3f58013346..f56c99b5fccb5 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -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) { diff --git a/packages/react/src/ReactTransitionType.js b/packages/react/src/ReactTransitionType.js index 4d72bb54c5a27..33f3b2d22fd8e 100644 --- a/packages/react/src/ReactTransitionType.js +++ b/packages/react/src/ReactTransitionType.js @@ -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;