@samitouri / QOS-React / commits / 3607f4838a

Add Commit Scaffolding for Gestures (#32451)

This adds a `ReactFiberApplyGesture` which is basically intended to be a fork of the phases in `ReactFiberCommitWork` except for the fake commit that `useSwipeTransition` does. So far none of the phases are actually implemented yet. This is just the scaffolding around them so I can fill them in later. The important bit is that we call `startViewTransition` (via the `startGestureTransition` Config) when a gesture starts. We add a paused animation to prevent the transition from committing (even if the ScrollTimeline goes to 100%). This also locks the documents so that we can't commit any other Transitions until it completes. When the gesture completes (scroll end) then we stop the gesture View Transition. If there's no new work scheduled we do that immediately but if there was any new work already scheduled, then we assume that this will potentially commit the new state. So we wait for that to finish. This lets us lock the animation in its state instead of snapping back and then applying the real update. Using this technique we can't actually run a View Transition from the current state to the actual committed state because it would snap back to the beginning and then run the View Transition from there. Therefore any new commit needs to skip View Transitions even if it should've technically animated to that state. We assume that the new state is the same as the optimistic state you already swiped to. An alternative to this technique could be to commit the optimistic state when we cancel and then apply any new updates o top of that. I might explore that in the future. Regardless it's important that the `action` associated with the swipe schedules some work before we cancel. Otherwise it risks reverting first. So I had to update this in the fixture.

Sebastian Markbåge committed Feb 27, 2025 at 16:45 UTC 3607f4838a8f4a87160da36aa26bb1432d7a5f11
14 files changed +401 -50
fixtures/view-transition/src/components/SwipeRecognizer.js
+5 -5
@@ -33,11 +33,6 @@ export default function SwipeRecognizer({
33 });
34 }
35 function onScrollEnd() {
36 - if (activeGesture.current !== null) {
37 - const cancelGesture = activeGesture.current;
38 - activeGesture.current = null;
39 - cancelGesture();
40 - }
36 let changed;
37 const scrollElement = scrollRef.current;
38 if (axis === 'x') {
@@ -60,6 +55,11 @@ export default function SwipeRecognizer({
55 // Trigger side-effects
56 startTransition(action);
57 }
58 + if (activeGesture.current !== null) {
59 + const cancelGesture = activeGesture.current;
60 + activeGesture.current = null;
61 + cancelGesture();
62 + }
63 }
64
65 useEffect(() => {
packages/react-art/src/ReactFiberConfigART.js
+6
@@ -500,6 +500,12 @@ export function startViewTransition() {
500 return false;
501 }
502
503 +export type RunningGestureTransition = null;
504 +
505 +export function startGestureTransition() {}
506 +
507 +export function stopGestureTransition(transition: RunningGestureTransition) {}
508 +
509 export type ViewTransitionInstance = null | {name: string, ...};
510
511 export function createViewTransitionInstance(
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+73 -1
@@ -1394,7 +1394,10 @@ export function startViewTransition(
1394 transition.ready.then(spawnedWorkCallback, spawnedWorkCallback);
1395 transition.finished.then(() => {
1396 // $FlowFixMe[prop-missing]
1397 - ownerDocument.__reactViewTransition = null;
1397 + if (ownerDocument.__reactViewTransition === transition) {
1398 + // $FlowFixMe[prop-missing]
1399 + ownerDocument.__reactViewTransition = null;
1400 + }
1401 passiveCallback();
1402 });
1403 return true;
@@ -1409,6 +1412,75 @@ export function startViewTransition(
1412 }
1413 }
1414
1415 +export type RunningGestureTransition = {
1416 + skipTransition(): void,
1417 + ...
1418 +};
1419 +
1420 +export function startGestureTransition(
1421 + rootContainer: Container,
1422 + transitionTypes: null | TransitionTypes,
1423 + mutationCallback: () => void,
1424 + animateCallback: () => void,
1425 +): null | RunningGestureTransition {
1426 + const ownerDocument: Document =
1427 + rootContainer.nodeType === DOCUMENT_NODE
1428 + ? (rootContainer: any)
1429 + : rootContainer.ownerDocument;
1430 + try {
1431 + // $FlowFixMe[prop-missing]
1432 + const transition = ownerDocument.startViewTransition({
1433 + update: mutationCallback,
1434 + types: transitionTypes,
1435 + });
1436 + // $FlowFixMe[prop-missing]
1437 + ownerDocument.__reactViewTransition = transition;
1438 + let blockingAnim = null;
1439 + const readyCallback = () => {
1440 + // View Transitions with ScrollTimeline has a quirk where they end if the
1441 + // ScrollTimeline ever reaches 100% but that doesn't mean we're done because
1442 + // you can swipe back again. We can prevent this by adding a paused Animation
1443 + // that never stops. This seems to keep all running Animations alive until
1444 + // we explicitly abort (or something forces the View Transition to cancel).
1445 + const documentElement: Element = (ownerDocument.documentElement: any);
1446 + blockingAnim = documentElement.animate([{}, {}], {
1447 + pseudoElement: '::view-transition',
1448 + duration: 1,
1449 + });
1450 + blockingAnim.pause();
1451 + animateCallback();
1452 + };
1453 + transition.ready.then(readyCallback, readyCallback);
1454 + transition.finished.then(() => {
1455 + if (blockingAnim !== null) {
1456 + // In Safari, we need to manually clear this or it'll block future transitions.
1457 + blockingAnim.cancel();
1458 + }
1459 + // $FlowFixMe[prop-missing]
1460 + if (ownerDocument.__reactViewTransition === transition) {
1461 + // $FlowFixMe[prop-missing]
1462 + ownerDocument.__reactViewTransition = null;
1463 + }
1464 + });
1465 + return transition;
1466 + } catch (x) {
1467 + // We use the error as feature detection.
1468 + // The only thing that should throw is if startViewTransition is missing
1469 + // or if it doesn't accept the object form. Other errors are async.
1470 + // I.e. it's before the View Transitions v2 spec. We only support View
1471 + // Transitions v2 otherwise we fallback to not animating to ensure that
1472 + // we're not animating with the wrong animation mapped.
1473 + // Run through the sequence to put state back into a consistent state.
1474 + mutationCallback();
1475 + animateCallback();
1476 + return null;
1477 + }
1478 +}
1479 +
1480 +export function stopGestureTransition(transition: RunningGestureTransition) {
1481 + transition.skipTransition();
1482 +}
1483 +
1484 interface ViewTransitionPseudoElementType extends Animatable {
1485 _scope: HTMLElement;
1486 _selector: string;
packages/react-native-renderer/src/ReactFiberConfigNative.js
+15
@@ -597,6 +597,21 @@ export function startViewTransition(
597 return false;
598 }
599
600 +export type RunningGestureTransition = null;
601 +
602 +export function startGestureTransition(
603 + rootContainer: Container,
604 + transitionTypes: null | TransitionTypes,
605 + mutationCallback: () => void,
606 + animateCallback: () => void,
607 +): RunningGestureTransition {
608 + mutationCallback();
609 + animateCallback();
610 + return null;
611 +}
612 +
613 +export function stopGestureTransition(transition: RunningGestureTransition) {}
614 +
615 export type ViewTransitionInstance = null | {name: string, ...};
616
617 export function createViewTransitionInstance(
packages/react-noop-renderer/src/createReactNoop.js
+15
@@ -93,6 +93,8 @@ export type TransitionStatus = mixed;
93
94 export type FormInstance = Instance;
95
96 +export type RunningGestureTransition = null;
97 +
98 export type ViewTransitionInstance = null | {name: string, ...};
99
100 export type GestureTimeline = null;
@@ -792,6 +794,19 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
794 return false;
795 },
796
797 + startGestureTransition(
798 + rootContainer: Container,
799 + transitionTypes: null | TransitionTypes,
800 + mutationCallback: () => void,
801 + animateCallback: () => void,
802 + ): RunningGestureTransition {
803 + mutationCallback();
804 + animateCallback();
805 + return null;
806 + },
807 +
808 + stopGestureTransition(transition: RunningGestureTransition) {},
809 +
810 createViewTransitionInstance(name: string): ViewTransitionInstance {
811 return null;
812 },
packages/react-reconciler/src/ReactFiberApplyGesture.js new
+42
@@ -0,0 +1,42 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Fiber, FiberRoot} from './ReactInternalTypes';
11 +
12 +import {
13 + cancelRootViewTransitionName,
14 + restoreRootViewTransitionName,
15 +} from './ReactFiberConfig';
16 +
17 +// Clone View Transition boundaries that have any mutations or might have had their
18 +// layout affected by child insertions.
19 +export function insertDestinationClones(
20 + root: FiberRoot,
21 + finishedWork: Fiber,
22 +): void {
23 + // TODO
24 +}
25 +
26 +// Revert insertions and apply view transition names to the "new" (current) state.
27 +export function applyDepartureTransitions(
28 + root: FiberRoot,
29 + finishedWork: Fiber,
30 +): void {
31 + // TODO
32 + cancelRootViewTransitionName(root.containerInfo);
33 +}
34 +
35 +// Revert transition names and start/adjust animations on the started View Transition.
36 +export function startGestureAnimations(
37 + root: FiberRoot,
38 + finishedWork: Fiber,
39 +): void {
40 + // TODO
41 + restoreRootViewTransitionName(root.containerInfo);
42 +}
packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js
+3
@@ -46,6 +46,9 @@ export const wasInstanceInViewport = shim;
46 export const hasInstanceChanged = shim;
47 export const hasInstanceAffectedParent = shim;
48 export const startViewTransition = shim;
49 +export type RunningGestureTransition = null;
50 +export const startGestureTransition = shim;
51 +export const stopGestureTransition = shim;
52 export type ViewTransitionInstance = null | {name: string, ...};
53 export const createViewTransitionInstance = shim;
54 export type GestureTimeline = any;
packages/react-reconciler/src/ReactFiberGestureScheduler.js
+68 -12
@@ -8,11 +8,21 @@
8 */
9
10 import type {FiberRoot} from './ReactInternalTypes';
11 -import type {GestureTimeline} from './ReactFiberConfig';
11 +import type {
12 + GestureTimeline,
13 + RunningGestureTransition,
14 +} from './ReactFiberConfig';
15
13 -import {GestureLane} from './ReactFiberLane';
16 +import {
17 + GestureLane,
18 + includesBlockingLane,
19 + includesTransitionLane,
20 +} from './ReactFiberLane';
21 import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
15 -import {subscribeToGestureDirection} from './ReactFiberConfig';
22 +import {
23 + subscribeToGestureDirection,
24 + stopGestureTransition,
25 +} from './ReactFiberConfig';
26
27 // This type keeps track of any scheduled or active gestures.
28 export type ScheduledGesture = {
@@ -23,6 +33,7 @@ export type ScheduledGesture = {
33 rangeCurrent: number, // The starting offset along the timeline.
34 rangeNext: number, // The end along the timeline where the next state is reached.
35 cancel: () => void, // Cancel the subscription to direction change.
36 + running: null | RunningGestureTransition, // Used to cancel the running transition after we're done.
37 prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
38 next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
39 };
@@ -35,7 +46,7 @@ export function scheduleGesture(
46 rangeCurrent: number,
47 rangeNext: number,
48 ): ScheduledGesture {
38 - let prev = root.gestures;
49 + let prev = root.pendingGestures;
50 while (prev !== null) {
51 if (prev.provider === provider) {
52 // Existing instance found.
@@ -59,16 +70,16 @@ export function scheduleGesture(
70 }
71 if (gesture.direction !== direction) {
72 gesture.direction = direction;
62 - if (gesture.prev === null && root.gestures !== gesture) {
73 + if (gesture.prev === null && root.pendingGestures !== gesture) {
74 // This gesture is not in the schedule, meaning it was already rendered.
75 // We need to rerender in the new direction. Insert it into the first slot
76 // in case other gestures are queued after the on-going one.
66 - const existing = root.gestures;
77 + const existing = root.pendingGestures;
78 gesture.next = existing;
79 if (existing !== null) {
80 existing.prev = gesture;
81 }
71 - root.gestures = gesture;
82 + root.pendingGestures = gesture;
83 // Schedule the lane on the root. The Fibers will already be marked as
84 // long as the gesture is active on that Hook.
85 root.pendingLanes |= GestureLane;
@@ -86,11 +97,12 @@ export function scheduleGesture(
97 rangeCurrent: rangeCurrent,
98 rangeNext: rangeNext,
99 cancel: cancel,
100 + running: null,
101 prev: prev,
102 next: null,
103 };
104 if (prev === null) {
93 - root.gestures = gesture;
105 + root.pendingGestures = gesture;
106 } else {
107 prev.next = gesture;
108 }
@@ -106,10 +118,35 @@ export function cancelScheduledGesture(
118 if (gesture.count === 0) {
119 const cancelDirectionSubscription = gesture.cancel;
120 cancelDirectionSubscription();
109 - // Delete the scheduled gesture from the queue.
121 + // Delete the scheduled gesture from the pending queue.
122 deleteScheduledGesture(root, gesture);
123 // TODO: If we're currently rendering this gesture, we need to restart the render
124 // on a different gesture or cancel the render..
125 + // TODO: We might want to pause the View Transition at this point since you should
126 + // no longer be able to update the position of anything but it might be better to
127 + // just commit the gesture state.
128 + const runningTransition = gesture.running;
129 + if (runningTransition !== null) {
130 + const pendingLanesExcludingGestureLane = root.pendingLanes & ~GestureLane;
131 + if (
132 + includesBlockingLane(pendingLanesExcludingGestureLane) ||
133 + includesTransitionLane(pendingLanesExcludingGestureLane)
134 + ) {
135 + // If we have pending work we schedule the gesture to be stopped at the next commit.
136 + // This ensures that we don't snap back to the previous state until we have
137 + // had a chance to commit any resulting updates.
138 + const existing = root.stoppingGestures;
139 + if (existing !== null) {
140 + gesture.next = existing;
141 + existing.prev = gesture;
142 + }
143 + root.stoppingGestures = gesture;
144 + } else {
145 + gesture.running = null;
146 + // If there's no work scheduled so we can stop the View Transition right away.
147 + stopGestureTransition(runningTransition);
148 + }
149 + }
150 }
151 }
152
@@ -118,15 +155,19 @@ export function deleteScheduledGesture(
155 gesture: ScheduledGesture,
156 ): void {
157 if (gesture.prev === null) {
121 - if (root.gestures === gesture) {
122 - root.gestures = gesture.next;
123 - if (root.gestures === null) {
158 + if (root.pendingGestures === gesture) {
159 + root.pendingGestures = gesture.next;
160 + if (root.pendingGestures === null) {
161 // Gestures don't clear their lanes while the gesture is still active but it
162 // might not be scheduled to do any more renders and so we shouldn't schedule
163 // any more gesture lane work until a new gesture is scheduled.
164 root.pendingLanes &= ~GestureLane;
165 }
166 }
167 + if (root.stoppingGestures === gesture) {
168 + // This should not really happen the way we use it now but just in case we start.
169 + root.stoppingGestures = gesture.next;
170 + }
171 } else {
172 gesture.prev.next = gesture.next;
173 if (gesture.next !== null) {
@@ -136,3 +177,18 @@ export function deleteScheduledGesture(
177 gesture.next = null;
178 }
179 }
180 +
181 +export function stopCompletedGestures(root: FiberRoot) {
182 + let gesture = root.stoppingGestures;
183 + root.stoppingGestures = null;
184 + while (gesture !== null) {
185 + if (gesture.running !== null) {
186 + stopGestureTransition(gesture.running);
187 + gesture.running = null;
188 + }
189 + const nextGesture = gesture.next;
190 + gesture.next = null;
191 + gesture.prev = null;
192 + gesture = nextGesture;
193 + }
194 +}
packages/react-reconciler/src/ReactFiberHooks.js
+1 -1
@@ -4126,7 +4126,7 @@ function updateSwipeTransition<T>(
4126 );
4127 }
4128 // We assume that the currently rendering gesture is the one first in the queue.
4129 - const rootRenderGesture = root.gestures;
4129 + const rootRenderGesture = root.pendingGestures;
4130 if (rootRenderGesture !== null) {
4131 let update = queue.pending;
4132 while (update !== null) {
packages/react-reconciler/src/ReactFiberRoot.js
+2 -1
@@ -99,7 +99,8 @@ function FiberRootNode(
99 this.formState = formState;
100
101 if (enableSwipeTransition) {
102 - this.gestures = null;
102 + this.pendingGestures = null;
103 + this.stoppingGestures = null;
104 }
105
106 this.incompleteTransitions = new Map();
packages/react-reconciler/src/ReactFiberWorkLoop.js
+151 -29
@@ -100,6 +100,7 @@ import {
100 resolveUpdatePriority,
101 trackSchedulerEvent,
102 startViewTransition,
103 + startGestureTransition,
104 createViewTransitionInstance,
105 } from './ReactFiberConfig';
106
@@ -228,6 +229,11 @@ import {
229 accumulateSuspenseyCommit,
230 } from './ReactFiberCommitWork';
231 import {shouldStartViewTransition} from './ReactFiberCommitViewTransitions';
232 +import {
233 + insertDestinationClones,
234 + applyDepartureTransitions,
235 + startGestureAnimations,
236 +} from './ReactFiberApplyGesture';
237 import {enqueueUpdate} from './ReactFiberClassUpdateQueue';
238 import {resetContextDependencies} from './ReactFiberNewContext';
239 import {
@@ -341,7 +347,10 @@ import {
347 import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
348 import {peekEntangledActionLane} from './ReactFiberAsyncAction';
349 import {logUncaughtError} from './ReactFiberErrorLogger';
344 -import {deleteScheduledGesture} from './ReactFiberGestureScheduler';
350 +import {
351 + deleteScheduledGesture,
352 + stopCompletedGestures,
353 +} from './ReactFiberGestureScheduler';
354
355 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
356
@@ -644,7 +653,9 @@ const PENDING_LAYOUT_PHASE = 2;
653 const PENDING_AFTER_MUTATION_PHASE = 3;
654 const PENDING_SPAWNED_WORK = 4;
655 const PENDING_PASSIVE_PHASE = 5;
647 -let pendingEffectsStatus: 0 | 1 | 2 | 3 | 4 | 5 = 0;
656 +const PENDING_GESTURE_MUTATION_PHASE = 6;
657 +const PENDING_GESTURE_ANIMATION_PHASE = 7;
658 +let pendingEffectsStatus: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 = 0;
659 let pendingEffectsRoot: FiberRoot = (null: any);
660 let pendingFinishedWork: Fiber = (null: any);
661 let pendingEffectsLanes: Lanes = NoLanes;
@@ -1424,11 +1435,12 @@ function commitRootWhenReady(
1435 const subtreeFlags = finishedWork.subtreeFlags;
1436 const isViewTransitionEligible =
1437 enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes); // TODO: Use a subtreeFlag to optimize.
1438 + const isGestureTransition = enableSwipeTransition && isGestureRender(lanes);
1439 const maySuspendCommit =
1440 subtreeFlags & ShouldSuspendCommit ||
1441 (subtreeFlags & BothVisibilityAndMaySuspendCommit) ===
1442 BothVisibilityAndMaySuspendCommit;
1431 - if (isViewTransitionEligible || maySuspendCommit) {
1443 + if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) {
1444 // Before committing, ask the renderer whether the host tree is ready.
1445 // If it's not, we'll wait until it notifies us.
1446 startSuspendingCommit();
@@ -1439,8 +1451,12 @@ function commitRootWhenReady(
1451 // This will also track any newly added or appearing ViewTransition
1452 // components for the purposes of forming pairs.
1453 accumulateSuspenseyCommit(finishedWork);
1442 - if (isViewTransitionEligible) {
1443 - suspendOnActiveViewTransition(root.containerInfo);
1454 + if (isViewTransitionEligible || isGestureTransition) {
1455 + // If we're stopping gestures we don't have to wait for any pending
1456 + // view transition. We'll stop it when we commit.
1457 + if (!enableSwipeTransition || root.stoppingGestures === null) {
1458 + suspendOnActiveViewTransition(root.containerInfo);
1459 + }
1460 }
1461 // At the end, ask the renderer if it's ready to commit, or if we should
1462 // suspend. If it's not ready, it will return a callback to subscribe to
@@ -3263,6 +3279,12 @@ function commitRoot(
3279 if (enableSchedulingProfiler) {
3280 markCommitStopped();
3281 }
3282 + if (enableSwipeTransition) {
3283 + // Stop any gestures that were completed and is now being reverted.
3284 + if (root.stoppingGestures !== null) {
3285 + stopCompletedGestures(root);
3286 + }
3287 + }
3288 return;
3289 } else {
3290 if (__DEV__) {
@@ -3291,7 +3313,7 @@ function commitRoot(
3313 const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
3314 remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
3315
3294 - if (enableSwipeTransition && root.gestures === null) {
3316 + if (enableSwipeTransition && root.pendingGestures === null) {
3317 // Gestures don't clear their lanes while the gesture is still active but it
3318 // might not be scheduled to do any more renders and so we shouldn't schedule
3319 // any more gesture lane work until a new gesture is scheduled.
@@ -3321,21 +3343,6 @@ function commitRoot(
3343 // times out.
3344 }
3345
3324 - if (enableSwipeTransition && isGestureRender(lanes)) {
3325 - // This is a special kind of render that doesn't commit regular effects.
3326 - commitGestureOnRoot(
3327 - root,
3328 - finishedWork,
3329 - recoverableErrors,
3330 - enableProfilerTimer
3331 - ? suspendedCommitReason === IMMEDIATE_COMMIT
3332 - ? completedRenderEndTime
3333 - : commitStartTime
3334 - : 0,
3335 - );
3336 - return;
3337 - }
3338 -
3346 // workInProgressX might be overwritten, so we want
3347 // to store it in pendingPassiveX until they get processed
3348 // We need to pass this through as an argument to commitRoot
@@ -3354,6 +3361,21 @@ function commitRoot(
3361 pendingSuspendedCommitReason = suspendedCommitReason;
3362 }
3363
3364 + if (enableSwipeTransition && isGestureRender(lanes)) {
3365 + // This is a special kind of render that doesn't commit regular effects.
3366 + commitGestureOnRoot(
3367 + root,
3368 + finishedWork,
3369 + recoverableErrors,
3370 + enableProfilerTimer
3371 + ? suspendedCommitReason === IMMEDIATE_COMMIT
3372 + ? completedRenderEndTime
3373 + : commitStartTime
3374 + : 0,
3375 + );
3376 + return;
3377 + }
3378 +
3379 // If there are pending passive effects, schedule a callback to process them.
3380 // Do this as early as possible, so it is queued before anything else that
3381 // might get scheduled in the commit phase. (See #16714.)
@@ -3461,10 +3483,23 @@ function commitRoot(
3483 ReactSharedInternals.T = prevTransition;
3484 }
3485 }
3486 +
3487 + let willStartViewTransition = shouldStartViewTransition;
3488 + if (enableSwipeTransition) {
3489 + // Stop any gestures that were completed and is now being committed.
3490 + if (root.stoppingGestures !== null) {
3491 + stopCompletedGestures(root);
3492 + // If we are in the process of stopping some gesture we shouldn't start
3493 + // a View Transition because that would start from the previous state to
3494 + // the next state.
3495 + willStartViewTransition = false;
3496 + }
3497 + }
3498 +
3499 pendingEffectsStatus = PENDING_MUTATION_PHASE;
3500 const startedViewTransition =
3501 enableViewTransition &&
3467 - shouldStartViewTransition &&
3502 + willStartViewTransition &&
3503 startViewTransition(
3504 root.containerInfo,
3505 pendingTransitionTypes,
@@ -3633,6 +3668,7 @@ function flushSpawnedWork(): void {
3668 } else {
3669 pendingEffectsStatus = NO_PENDING_EFFECTS;
3670 pendingEffectsRoot = (null: any); // Clear for GC purposes.
3671 + pendingFinishedWork = (null: any); // Clear for GC purposes.
3672 // There were no passive effects, so we can immediately release the cache
3673 // pool for this render.
3674 releaseRootPooledCache(root, root.pendingLanes);
@@ -3830,20 +3866,103 @@ function flushSpawnedWork(): void {
3866
3867 function commitGestureOnRoot(
3868 root: FiberRoot,
3833 - finishedWork: null | Fiber,
3869 + finishedWork: Fiber,
3870 recoverableErrors: null | Array<CapturedValue<mixed>>,
3871 renderEndTime: number, // Profiling-only
3872 ): void {
3873 // We assume that the gesture we just rendered was the first one in the queue.
3838 - const finishedGesture = root.gestures;
3874 + const finishedGesture = root.pendingGestures;
3875 if (finishedGesture === null) {
3840 - throw new Error(
3841 - 'Finished rendering the gesture lane but there were no pending gestures. ' +
3842 - 'React should not have started a render in this case. This is a bug in React.',
3843 - );
3876 + // We must have already cancelled this gesture before we had a chance to
3877 + // render it. Let's schedule work on the next set of lanes.
3878 + ensureRootIsScheduled(root);
3879 + return;
3880 }
3881 deleteScheduledGesture(root, finishedGesture);
3846 - // TODO: Run the gesture
3882 +
3883 + const prevTransition = ReactSharedInternals.T;
3884 + ReactSharedInternals.T = null;
3885 + const previousPriority = getCurrentUpdatePriority();
3886 + setCurrentUpdatePriority(DiscreteEventPriority);
3887 + const prevExecutionContext = executionContext;
3888 + executionContext |= CommitContext;
3889 + try {
3890 + insertDestinationClones(root, finishedWork);
3891 + } finally {
3892 + // Reset the priority to the previous non-sync value.
3893 + executionContext = prevExecutionContext;
3894 + setCurrentUpdatePriority(previousPriority);
3895 + ReactSharedInternals.T = prevTransition;
3896 + }
3897 + // TODO: Collect transition types.
3898 + pendingTransitionTypes = null;
3899 + pendingEffectsStatus = PENDING_GESTURE_MUTATION_PHASE;
3900 +
3901 + finishedGesture.running = startGestureTransition(
3902 + root.containerInfo,
3903 + pendingTransitionTypes,
3904 + flushGestureMutations,
3905 + flushGestureAnimations,
3906 + );
3907 +}
3908 +
3909 +function flushGestureMutations(): void {
3910 + if (pendingEffectsStatus !== PENDING_GESTURE_MUTATION_PHASE) {
3911 + return;
3912 + }
3913 + pendingEffectsStatus = NO_PENDING_EFFECTS;
3914 + const root = pendingEffectsRoot;
3915 + const finishedWork = pendingFinishedWork;
3916 +
3917 + const prevTransition = ReactSharedInternals.T;
3918 + ReactSharedInternals.T = null;
3919 + const previousPriority = getCurrentUpdatePriority();
3920 + setCurrentUpdatePriority(DiscreteEventPriority);
3921 + const prevExecutionContext = executionContext;
3922 + executionContext |= CommitContext;
3923 + try {
3924 + applyDepartureTransitions(root, finishedWork);
3925 + } finally {
3926 + // Reset the priority to the previous non-sync value.
3927 + executionContext = prevExecutionContext;
3928 + setCurrentUpdatePriority(previousPriority);
3929 + ReactSharedInternals.T = prevTransition;
3930 + }
3931 +
3932 + pendingEffectsStatus = PENDING_GESTURE_ANIMATION_PHASE;
3933 +}
3934 +
3935 +function flushGestureAnimations(): void {
3936 + // If we get canceled before we start we might not have applied
3937 + // mutations yet. We need to apply them first.
3938 + flushGestureMutations();
3939 + if (pendingEffectsStatus !== PENDING_GESTURE_ANIMATION_PHASE) {
3940 + return;
3941 + }
3942 + pendingEffectsStatus = NO_PENDING_EFFECTS;
3943 + const root = pendingEffectsRoot;
3944 + const finishedWork = pendingFinishedWork;
3945 + pendingEffectsRoot = (null: any); // Clear for GC purposes.
3946 + pendingFinishedWork = (null: any); // Clear for GC purposes.
3947 + pendingEffectsLanes = NoLanes;
3948 +
3949 + const prevTransition = ReactSharedInternals.T;
3950 + ReactSharedInternals.T = null;
3951 + const previousPriority = getCurrentUpdatePriority();
3952 + setCurrentUpdatePriority(DiscreteEventPriority);
3953 + const prevExecutionContext = executionContext;
3954 + executionContext |= CommitContext;
3955 + try {
3956 + startGestureAnimations(root, finishedWork);
3957 + } finally {
3958 + // Reset the priority to the previous non-sync value.
3959 + executionContext = prevExecutionContext;
3960 + setCurrentUpdatePriority(previousPriority);
3961 + ReactSharedInternals.T = prevTransition;
3962 + }
3963 +
3964 + // Now that we've rendered this lane. Start working on the next lane.
3965 + ensureRootIsScheduled(root);
3966 }
3967
3968 function makeErrorInfo(componentStack: ?string) {
@@ -3879,6 +3998,8 @@ function releaseRootPooledCache(root: FiberRoot, remainingLanes: Lanes) {
3998
3999 export function flushPendingEffects(wasDelayedCommit?: boolean): boolean {
4000 // Returns whether passive effects were flushed.
4001 + flushGestureMutations();
4002 + flushGestureAnimations();
4003 flushMutationEffects();
4004 flushLayoutEffects();
4005 // Skip flushAfterMutation if we're forcing this early.
@@ -3932,6 +4053,7 @@ function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
4053 const lanes = pendingEffectsLanes;
4054 pendingEffectsStatus = NO_PENDING_EFFECTS;
4055 pendingEffectsRoot = (null: any); // Clear for GC purposes.
4056 + pendingFinishedWork = (null: any); // Clear for GC purposes.
4057 // TODO: This is sometimes out of sync with pendingEffectsRoot.
4058 // Figure out why and fix it. It's not causing any known issues (probably
4059 // because it's only used for profiling), but it's a refactor hazard.
packages/react-reconciler/src/ReactInternalTypes.js
+2 -1
@@ -284,7 +284,8 @@ type BaseFiberRootProperties = {
284 formState: ReactFormState<any, any> | null,
285
286 // enableSwipeTransition only
287 - gestures: null | ScheduledGesture,
287 + pendingGestures: null | ScheduledGesture,
288 + stoppingGestures: null | ScheduledGesture,
289 };
290
291 // The following attributes are only used by DevTools and are only present in DEV builds.
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+3
@@ -40,6 +40,7 @@ export opaque type NoTimeout = mixed;
40 export opaque type RendererInspectionConfig = mixed;
41 export opaque type TransitionStatus = mixed;
42 export opaque type FormInstance = mixed;
43 +export type RunningGestureTransition = mixed;
44 export type ViewTransitionInstance = null | {name: string, ...};
45 export opaque type InstanceMeasurement = mixed;
46 export type EventResponder = any;
@@ -145,6 +146,8 @@ export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
146 export const hasInstanceChanged = $$$config.hasInstanceChanged;
147 export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
148 export const startViewTransition = $$$config.startViewTransition;
149 +export const startGestureTransition = $$$config.startGestureTransition;
150 +export const stopGestureTransition = $$$config.stopGestureTransition;
151 export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
152 export const subscribeToGestureDirection =
153 $$$config.subscribeToGestureDirection;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+15
@@ -375,6 +375,21 @@ export function startViewTransition(
375 return false;
376 }
377
378 +export type RunningGestureTransition = null;
379 +
380 +export function startGestureTransition(
381 + rootContainer: Container,
382 + transitionTypes: null | TransitionTypes,
383 + mutationCallback: () => void,
384 + animateCallback: () => void,
385 +): RunningGestureTransition {
386 + mutationCallback();
387 + animateCallback();
388 + return null;
389 +}
390 +
391 +export function stopGestureTransition(transition: RunningGestureTransition) {}
392 +
393 export type ViewTransitionInstance = null | {name: string, ...};
394
395 export function createViewTransitionInstance(