From 7accce602298386aab14c37a2000af85b76e5718 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 30 Mar 2025 11:46:40 -0400 Subject: [PATCH 1/5] Add TransitionTypes to startGestureTransition --- packages/react/src/ReactTransitionType.js | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/react/src/ReactTransitionType.js b/packages/react/src/ReactTransitionType.js index 85a9f8682f4ce..dfd5b3390fbf0 100644 --- a/packages/react/src/ReactTransitionType.js +++ b/packages/react/src/ReactTransitionType.js @@ -8,17 +8,32 @@ */ import ReactSharedInternals from 'shared/ReactSharedInternals'; -import {enableViewTransition} from 'shared/ReactFeatureFlags'; +import { + enableViewTransition, + enableGestureTransition, +} from 'shared/ReactFeatureFlags'; export type TransitionTypes = Array; +// This one is only available synchronously so we don't need to use ReactSharedInternals +// for this state. Instead, we track it in isomorphic and pass it to the renderer. +export let pendingGestureTransitionTypes: null | TransitionTypes = null; + export function addTransitionType(type: string): void { if (enableViewTransition) { - const pendingTransitionTypes: null | TransitionTypes = - ReactSharedInternals.V; - if (pendingTransitionTypes === null) { - ReactSharedInternals.V = [type]; - } else if (pendingTransitionTypes.indexOf(type) === -1) { + let pendingTransitionTypes: null | TransitionTypes = null; + if ( + enableGestureTransition && + ReactSharedInternals.T !== null && + ReactSharedInternals.T.gesture !== null + ) { + } else { + pendingTransitionTypes = ReactSharedInternals.V; + if (pendingTransitionTypes === null) { + pendingTransitionTypes = ReactSharedInternals.V = []; + } + } + if (pendingTransitionTypes.indexOf(type) === -1) { pendingTransitionTypes.push(type); } } From 6d926b474fb7c6af7c8c46c28ef5df87d5dab84a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 30 Mar 2025 11:59:42 -0400 Subject: [PATCH 2/5] Pass Transition Types added inside startGestureTransition to the renderer --- .../src/client/ReactFiberConfigDOM.js | 2 +- .../src/ReactFiberConfigNative.js | 2 +- .../src/createReactNoop.js | 2 +- .../src/ReactFiberTransition.js | 3 +++ .../src/ReactFiberWorkLoop.js | 2 +- .../src/ReactFiberConfigTestHost.js | 2 +- .../react/src/ReactSharedInternalsClient.js | 9 +++++++- packages/react/src/ReactStartTransition.js | 11 ++++++++++ packages/react/src/ReactTransitionType.js | 21 ++++++++++++++++++- 9 files changed, 47 insertions(+), 7 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index a8616833f6680..a031fc22e5367 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -25,7 +25,7 @@ import type { PreinitScriptOptions, PreinitModuleScriptOptions, } from 'react-dom/src/shared/ReactDOMTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType.js'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import {NotPending} from '../shared/ReactDOMFormActions'; diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index b278310c8b20c..b022d82d4e6c7 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -8,7 +8,7 @@ */ import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType.js'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; // Modules provided by RN: import { diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 405edb7c53293..57d86c6df5a9d 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -22,7 +22,7 @@ import type {UpdateQueue} from 'react-reconciler/src/ReactFiberClassUpdateQueue' import type {ReactNodeList} from 'shared/ReactTypes'; import type {RootTag} from 'react-reconciler/src/ReactRootTags'; import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; -import type {TransitionTypes} from 'react/src/ReactTransitionType.js'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import * as Scheduler from 'scheduler/unstable_mock'; import {REACT_FRAGMENT_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols'; diff --git a/packages/react-reconciler/src/ReactFiberTransition.js b/packages/react-reconciler/src/ReactFiberTransition.js index 4bb123205e1b3..221f8c028cb83 100644 --- a/packages/react-reconciler/src/ReactFiberTransition.js +++ b/packages/react-reconciler/src/ReactFiberTransition.js @@ -17,6 +17,7 @@ import type {StackCursor} from './ReactFiberStack'; import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent'; import type {Transition} from 'react/src/ReactStartTransition'; import type {ScheduledGesture} from './ReactFiberGestureScheduler'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import { enableTransitionTracing, @@ -112,6 +113,7 @@ if (enableGestureTransition) { transition: Transition, provider: GestureProvider, options: ?GestureOptions, + transitionTypes: null | TransitionTypes, ): () => void { let cancel = null; if (prevOnStartGestureTransitionFinish !== null) { @@ -119,6 +121,7 @@ if (enableGestureTransition) { transition, provider, options, + transitionTypes, ); } // For every root that has work scheduled, check if there's a ScheduledGesture diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 3f3f733b86667..9e8a78d419c5a 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -31,7 +31,7 @@ import { getViewTransitionName, type ViewTransitionState, } from './ReactFiberViewTransitionComponent'; -import type {TransitionTypes} from 'react/src/ReactTransitionType.js'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import { enableCreateEventHandleAPI, diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 9145d8e7e487a..c0ebf59601817 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -8,7 +8,7 @@ */ import type {ReactContext} from 'shared/ReactTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType.js'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import isArray from 'shared/isArray'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; diff --git a/packages/react/src/ReactSharedInternalsClient.js b/packages/react/src/ReactSharedInternalsClient.js index 8fba0750ae9d9..ca7bb6bad13fc 100644 --- a/packages/react/src/ReactSharedInternalsClient.js +++ b/packages/react/src/ReactSharedInternalsClient.js @@ -23,7 +23,14 @@ export type SharedStateClient = { A: null | AsyncDispatcher, // ReactCurrentCache for Cache T: null | Transition, // ReactCurrentBatchConfig for Transitions S: null | ((Transition, mixed) => void), // onStartTransitionFinish - G: null | ((Transition, GestureProvider, ?GestureOptions) => () => void), // onStartGestureTransitionFinish + G: + | null + | (( + Transition, + GestureProvider, + ?GestureOptions, + transitionTypes: null | TransitionTypes, + ) => () => void), // onStartGestureTransitionFinish V: null | TransitionTypes, // Pending Transition Types for the Next Transition // DEV-only diff --git a/packages/react/src/ReactStartTransition.js b/packages/react/src/ReactStartTransition.js index 249e4ca6d0dfd..f8a3f58013346 100644 --- a/packages/react/src/ReactStartTransition.js +++ b/packages/react/src/ReactStartTransition.js @@ -21,6 +21,12 @@ import { enableGestureTransition, } from 'shared/ReactFeatureFlags'; +import { + pendingGestureTransitionTypes, + pushPendingGestureTransitionTypes, + popPendingGestureTransitionTypes, +} from './ReactTransitionType'; + import reportGlobalError from 'shared/reportGlobalError'; export type Transition = { @@ -105,6 +111,8 @@ export function startGestureTransition( } ReactSharedInternals.T = currentTransition; + const prevTransitionTypes = pushPendingGestureTransitionTypes(); + try { const returnValue = scope(); if (__DEV__) { @@ -118,17 +126,20 @@ export function startGestureTransition( ); } } + const transitionTypes = pendingGestureTransitionTypes; const onStartGestureTransitionFinish = ReactSharedInternals.G; if (onStartGestureTransitionFinish !== null) { return onStartGestureTransitionFinish( currentTransition, provider, options, + transitionTypes, ); } } catch (error) { reportGlobalError(error); } finally { + popPendingGestureTransitionTypes(prevTransitionTypes); ReactSharedInternals.T = prevTransition; } return function cancelGesture() { diff --git a/packages/react/src/ReactTransitionType.js b/packages/react/src/ReactTransitionType.js index dfd5b3390fbf0..4d72bb54c5a27 100644 --- a/packages/react/src/ReactTransitionType.js +++ b/packages/react/src/ReactTransitionType.js @@ -19,15 +19,34 @@ export type TransitionTypes = Array; // for this state. Instead, we track it in isomorphic and pass it to the renderer. export let pendingGestureTransitionTypes: null | TransitionTypes = null; +export function pushPendingGestureTransitionTypes(): null | TransitionTypes { + const prev = pendingGestureTransitionTypes; + pendingGestureTransitionTypes = null; + return prev; +} + +export function popPendingGestureTransitionTypes( + prev: null | TransitionTypes, +): void { + pendingGestureTransitionTypes = prev; +} + export function addTransitionType(type: string): void { if (enableViewTransition) { - let pendingTransitionTypes: null | TransitionTypes = null; + let pendingTransitionTypes: null | TransitionTypes; if ( enableGestureTransition && ReactSharedInternals.T !== null && ReactSharedInternals.T.gesture !== null ) { + // We're inside a startGestureTransition which is always sync. + pendingTransitionTypes = pendingGestureTransitionTypes; + if (pendingTransitionTypes === null) { + pendingTransitionTypes = pendingGestureTransitionTypes = []; + } } else { + // Otherwise we're either inside a synchronous startTransition + // or in the async gap of one, which we track globally. pendingTransitionTypes = ReactSharedInternals.V; if (pendingTransitionTypes === null) { pendingTransitionTypes = ReactSharedInternals.V = []; From 1ee8c2cd97cebffe216c84bdec80fa09a65c1e83 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 30 Mar 2025 12:06:24 -0400 Subject: [PATCH 3/5] Collect TransitionTypes on the ScheduledGesture Each scheduled provider has its own set. --- .../src/ReactFiberGestureScheduler.js | 16 ++++++++++++++++ .../react-reconciler/src/ReactFiberTransition.js | 7 ++++++- .../react-reconciler/src/ReactFiberWorkLoop.js | 3 +-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberGestureScheduler.js b/packages/react-reconciler/src/ReactFiberGestureScheduler.js index 7018506c4752a..ed61c59b05ae6 100644 --- a/packages/react-reconciler/src/ReactFiberGestureScheduler.js +++ b/packages/react-reconciler/src/ReactFiberGestureScheduler.js @@ -10,6 +10,7 @@ import type {FiberRoot} from './ReactInternalTypes'; import type {GestureOptions} from 'shared/ReactTypes'; import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import { GestureLane, @@ -25,6 +26,7 @@ export type ScheduledGesture = { count: number, // The number of times this same provider has been started. rangeStart: number, // The percentage along the timeline where the "current" state starts. rangeEnd: number, // The percentage along the timeline where the "destination" state is reached. + types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition. running: null | RunningViewTransition, // Used to cancel the running transition after we're done. prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root. next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root. @@ -51,6 +53,7 @@ export function scheduleGesture( count: 0, rangeStart: 0, // Uninitialized rangeEnd: 100, // Uninitialized + types: null, running: null, prev: prev, next: null, @@ -68,6 +71,7 @@ export function startScheduledGesture( root: FiberRoot, gestureTimeline: GestureTimeline, gestureOptions: ?GestureOptions, + transitionTypes: null | TransitionTypes, ): null | ScheduledGesture { const rangeStart = gestureOptions && gestureOptions.rangeStart != null @@ -87,6 +91,18 @@ export function startScheduledGesture( // Update the options. prev.rangeStart = rangeStart; prev.rangeEnd = rangeEnd; + if (transitionTypes !== null) { + let scheduledTypes = prev.types; + if (scheduledTypes === null) { + scheduledTypes = prev.types = []; + } + for (let i = 0; i < transitionTypes.length; i++) { + const transitionType = transitionTypes[i]; + if (scheduledTypes.indexOf(transitionType) === -1) { + scheduledTypes.push(transitionType); + } + } + } return prev; } const next = prev.next; diff --git a/packages/react-reconciler/src/ReactFiberTransition.js b/packages/react-reconciler/src/ReactFiberTransition.js index 221f8c028cb83..128f5fa2d1b50 100644 --- a/packages/react-reconciler/src/ReactFiberTransition.js +++ b/packages/react-reconciler/src/ReactFiberTransition.js @@ -134,7 +134,12 @@ if (enableGestureTransition) { // that it's conceptually started globally. let root = firstScheduledRoot; while (root !== null) { - const scheduledGesture = startScheduledGesture(root, provider, options); + const scheduledGesture = startScheduledGesture( + root, + provider, + options, + transitionTypes, + ); if (scheduledGesture !== null) { cancel = chainGestureCancellation(root, scheduledGesture, cancel); } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 9e8a78d419c5a..f748f309df5c3 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -3925,8 +3925,7 @@ function commitGestureOnRoot( setCurrentUpdatePriority(previousPriority); ReactSharedInternals.T = prevTransition; } - // TODO: Collect transition types. - pendingTransitionTypes = null; + pendingTransitionTypes = finishedGesture.types; pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE; pendingViewTransition = finishedGesture.running = startGestureTransition( From fdf8ae3270197dd4833ebb1805a6b440252486d8 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 30 Mar 2025 13:42:51 -0400 Subject: [PATCH 4/5] Update fixture --- fixtures/view-transition/src/components/Page.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 483f01372fca9..3de87a949e5c7 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -1,4 +1,5 @@ import React, { + unstable_addTransitionType as addTransitionType, unstable_ViewTransition as ViewTransition, unstable_Activity as Activity, useLayoutEffect, @@ -113,7 +114,12 @@ export default function Page({url, navigate}) {
{ + addTransitionType( + direction === 'left' ? 'navigation-forward' : 'navigation-back' + ); + optimisticNavigate(direction); + }} direction={show ? 'left' : 'right'}>