@samitouri / QOS-React-1 / commits / 0b1a9e90c5

Support addTransitionType in startGestureTransition (#32792)

Stacked on #32788. Normally we track `addTransitionType` globally because of the async gap that can happen in Actions where we lack AsyncContext to associate it with a particular Transition. This unfortunately also means it's possible to call outside of `startTransition` which is something we want to warn for. We need to be able to distinguish whether `addTransitionType` is for a regular Transition or a Gesture Transition though. Since `startGestureTransition` is only synchronous we can track it within that execution scope and move it to a separate set. Since we know for sure which call owns it we can properly associate it with that specific provider's `ScheduledGesture`. This does not yet handle calling `addTransitionType` inside the render phase of a gesture. That would currently still be associated with the next Transition instead.

Sebastian Markbåge committed Apr 1, 2025 at 12:08 UTC 0b1a9e90c5d5b6a4633c225c9100af69f53752c8
11 files changed +99 -17
fixtures/view-transition/src/components/Page.js
+7 -1
@@ -1,4 +1,5 @@
1 import React, {
2 + unstable_addTransitionType as addTransitionType,
3 unstable_ViewTransition as ViewTransition,
4 unstable_Activity as Activity,
5 useLayoutEffect,
@@ -113,7 +114,12 @@ export default function Page({url, navigate}) {
114 <div className="swipe-recognizer">
115 <SwipeRecognizer
116 action={swipeAction}
116 - gesture={optimisticNavigate}
117 + gesture={direction => {
118 + addTransitionType(
119 + direction === 'left' ? 'navigation-forward' : 'navigation-back'
120 + );
121 + optimisticNavigate(direction);
122 + }}
123 direction={show ? 'left' : 'right'}>
124 <button
125 className="button"
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+1 -1
@@ -25,7 +25,7 @@ import type {
25 PreinitScriptOptions,
26 PreinitModuleScriptOptions,
27 } from 'react-dom/src/shared/ReactDOMTypes';
28 -import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
28 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
29
30 import {NotPending} from '../shared/ReactDOMFormActions';
31
packages/react-native-renderer/src/ReactFiberConfigNative.js
+1 -1
@@ -8,7 +8,7 @@
8 */
9
10 import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes';
11 -import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
11 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
12
13 // Modules provided by RN:
14 import {
packages/react-noop-renderer/src/createReactNoop.js
+1 -1
@@ -22,7 +22,7 @@ import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue'
22 import type {ReactNodeList} from 'shared/ReactTypes';
23 import type {RootTag} from 'react-reconciler/src/ReactRootTags';
24 import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
25 -import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
25 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
26
27 import * as Scheduler from 'scheduler/unstable_mock';
28 import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
packages/react-reconciler/src/ReactFiberGestureScheduler.js
+16
@@ -10,6 +10,7 @@
10 import type {FiberRoot} from './ReactInternalTypes';
11 import type {GestureOptions} from 'shared/ReactTypes';
12 import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
13 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
14
15 import {
16 GestureLane,
@@ -25,6 +26,7 @@ export type ScheduledGesture = {
26 count: number, // The number of times this same provider has been started.
27 rangeStart: number, // The percentage along the timeline where the "current" state starts.
28 rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
29 + types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
30 running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
31 prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
32 next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
@@ -51,6 +53,7 @@ export function scheduleGesture(
53 count: 0,
54 rangeStart: 0, // Uninitialized
55 rangeEnd: 100, // Uninitialized
56 + types: null,
57 running: null,
58 prev: prev,
59 next: null,
@@ -68,6 +71,7 @@ export function startScheduledGesture(
71 root: FiberRoot,
72 gestureTimeline: GestureTimeline,
73 gestureOptions: ?GestureOptions,
74 + transitionTypes: null | TransitionTypes,
75 ): null | ScheduledGesture {
76 const rangeStart =
77 gestureOptions && gestureOptions.rangeStart != null
@@ -87,6 +91,18 @@ export function startScheduledGesture(
91 // Update the options.
92 prev.rangeStart = rangeStart;
93 prev.rangeEnd = rangeEnd;
94 + if (transitionTypes !== null) {
95 + let scheduledTypes = prev.types;
96 + if (scheduledTypes === null) {
97 + scheduledTypes = prev.types = [];
98 + }
99 + for (let i = 0; i < transitionTypes.length; i++) {
100 + const transitionType = transitionTypes[i];
101 + if (scheduledTypes.indexOf(transitionType) === -1) {
102 + scheduledTypes.push(transitionType);
103 + }
104 + }
105 + }
106 return prev;
107 }
108 const next = prev.next;
packages/react-reconciler/src/ReactFiberTransition.js
+9 -1
@@ -17,6 +17,7 @@ import type {StackCursor} from './ReactFiberStack';
17 import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent';
18 import type {Transition} from 'react/src/ReactStartTransition';
19 import type {ScheduledGesture} from './ReactFiberGestureScheduler';
20 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
21
22 import {
23 enableTransitionTracing,
@@ -112,6 +113,7 @@ if (enableGestureTransition) {
113 transition: Transition,
114 provider: GestureProvider,
115 options: ?GestureOptions,
116 + transitionTypes: null | TransitionTypes,
117 ): () => void {
118 let cancel = null;
119 if (prevOnStartGestureTransitionFinish !== null) {
@@ -119,6 +121,7 @@ if (enableGestureTransition) {
121 transition,
122 provider,
123 options,
124 + transitionTypes,
125 );
126 }
127 // For every root that has work scheduled, check if there's a ScheduledGesture
@@ -131,7 +134,12 @@ if (enableGestureTransition) {
134 // that it's conceptually started globally.
135 let root = firstScheduledRoot;
136 while (root !== null) {
134 - const scheduledGesture = startScheduledGesture(root, provider, options);
137 + const scheduledGesture = startScheduledGesture(
138 + root,
139 + provider,
140 + options,
141 + transitionTypes,
142 + );
143 if (scheduledGesture !== null) {
144 cancel = chainGestureCancellation(root, scheduledGesture, cancel);
145 }
packages/react-reconciler/src/ReactFiberWorkLoop.js
+2 -3
@@ -31,7 +31,7 @@ import {
31 getViewTransitionName,
32 type ViewTransitionState,
33 } from './ReactFiberViewTransitionComponent';
34 -import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
34 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
35
36 import {
37 enableCreateEventHandleAPI,
@@ -3925,8 +3925,7 @@ function commitGestureOnRoot(
3925 setCurrentUpdatePriority(previousPriority);
3926 ReactSharedInternals.T = prevTransition;
3927 }
3928 - // TODO: Collect transition types.
3929 - pendingTransitionTypes = null;
3928 + pendingTransitionTypes = finishedGesture.types;
3929 pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;
3930
3931 pendingViewTransition = finishedGesture.running = startGestureTransition(
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+1 -1
@@ -8,7 +8,7 @@
8 */
9
10 import type {ReactContext} from 'shared/ReactTypes';
11 -import type {TransitionTypes} from 'react/src/ReactTransitionType.js';
11 +import type {TransitionTypes} from 'react/src/ReactTransitionType';
12
13 import isArray from 'shared/isArray';
14 import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols';
packages/react/src/ReactSharedInternalsClient.js
+10 -2
@@ -18,12 +18,20 @@ import {
18 enableGestureTransition,
19 } from 'shared/ReactFeatureFlags';
20
21 +type onStartTransitionFinish = (Transition, mixed) => void;
22 +type onStartGestureTransitionFinish = (
23 + Transition,
24 + GestureProvider,
25 + ?GestureOptions,
26 + transitionTypes: null | TransitionTypes,
27 +) => () => void;
28 +
29 export type SharedStateClient = {
30 H: null | Dispatcher, // ReactCurrentDispatcher for Hooks
31 A: null | AsyncDispatcher, // ReactCurrentCache for Cache
32 T: null | Transition, // ReactCurrentBatchConfig for Transitions
25 - S: null | ((Transition, mixed) => void), // onStartTransitionFinish
26 - G: null | ((Transition, GestureProvider, ?GestureOptions) => () => void), // onStartGestureTransitionFinish
33 + S: null | onStartTransitionFinish,
34 + G: null | onStartGestureTransitionFinish,
35 V: null | TransitionTypes, // Pending Transition Types for the Next Transition
36
37 // DEV-only
packages/react/src/ReactStartTransition.js
+11
@@ -21,6 +21,12 @@ import {
21 enableGestureTransition,
22 } from 'shared/ReactFeatureFlags';
23
24 +import {
25 + pendingGestureTransitionTypes,
26 + pushPendingGestureTransitionTypes,
27 + popPendingGestureTransitionTypes,
28 +} from './ReactTransitionType';
29 +
30 import reportGlobalError from 'shared/reportGlobalError';
31
32 export type Transition = {
@@ -105,6 +111,8 @@ export function startGestureTransition(
111 }
112 ReactSharedInternals.T = currentTransition;
113
114 + const prevTransitionTypes = pushPendingGestureTransitionTypes();
115 +
116 try {
117 const returnValue = scope();
118 if (__DEV__) {
@@ -118,17 +126,20 @@ export function startGestureTransition(
126 );
127 }
128 }
129 + const transitionTypes = pendingGestureTransitionTypes;
130 const onStartGestureTransitionFinish = ReactSharedInternals.G;
131 if (onStartGestureTransitionFinish !== null) {
132 return onStartGestureTransitionFinish(
133 currentTransition,
134 provider,
135 options,
136 + transitionTypes,
137 );
138 }
139 } catch (error) {
140 reportGlobalError(error);
141 } finally {
142 + popPendingGestureTransitionTypes(prevTransitionTypes);
143 ReactSharedInternals.T = prevTransition;
144 }
145 return function cancelGesture() {
packages/react/src/ReactTransitionType.js
+40 -6
@@ -8,17 +8,51 @@
8 */
9
10 import ReactSharedInternals from 'shared/ReactSharedInternals';
11 -import {enableViewTransition} from 'shared/ReactFeatureFlags';
11 +import {
12 + enableViewTransition,
13 + enableGestureTransition,
14 +} from 'shared/ReactFeatureFlags';
15
16 export type TransitionTypes = Array<string>;
17
18 +// This one is only available synchronously so we don't need to use ReactSharedInternals
19 +// for this state. Instead, we track it in isomorphic and pass it to the renderer.
20 +export let pendingGestureTransitionTypes: null | TransitionTypes = null;
21 +
22 +export function pushPendingGestureTransitionTypes(): null | TransitionTypes {
23 + const prev = pendingGestureTransitionTypes;
24 + pendingGestureTransitionTypes = null;
25 + return prev;
26 +}
27 +
28 +export function popPendingGestureTransitionTypes(
29 + prev: null | TransitionTypes,
30 +): void {
31 + pendingGestureTransitionTypes = prev;
32 +}
33 +
34 export function addTransitionType(type: string): void {
35 if (enableViewTransition) {
17 - const pendingTransitionTypes: null | TransitionTypes =
18 - ReactSharedInternals.V;
19 - if (pendingTransitionTypes === null) {
20 - ReactSharedInternals.V = [type];
21 - } else if (pendingTransitionTypes.indexOf(type) === -1) {
36 + let pendingTransitionTypes: null | TransitionTypes;
37 + if (
38 + enableGestureTransition &&
39 + ReactSharedInternals.T !== null &&
40 + ReactSharedInternals.T.gesture !== null
41 + ) {
42 + // We're inside a startGestureTransition which is always sync.
43 + pendingTransitionTypes = pendingGestureTransitionTypes;
44 + if (pendingTransitionTypes === null) {
45 + pendingTransitionTypes = pendingGestureTransitionTypes = [];
46 + }
47 + } else {
48 + // Otherwise we're either inside a synchronous startTransition
49 + // or in the async gap of one, which we track globally.
50 + pendingTransitionTypes = ReactSharedInternals.V;
51 + if (pendingTransitionTypes === null) {
52 + pendingTransitionTypes = ReactSharedInternals.V = [];
53 + }
54 + }
55 + if (pendingTransitionTypes.indexOf(type) === -1) {
56 pendingTransitionTypes.push(type);
57 }
58 }