@samitouri / QOS-React-1 / commits / 35a81cecf7

Entangle Gesture revert commit with the corresponding Action commit (#35487)

Stacked on #35486. When a Gesture commits, it leaves behind work on a Transition lane (`revertLane`). This entangles that lane with whatever lane we're using in the event that cancels the Gesture. This ensures that the revert and the result of any resulting Action commits as one batch. Typically the Action would apply a new state that is similar or the same as the revert of the Gesture. This makes it resilient to unbatching in #35392.

Sebastian Markbåge committed Jan 15, 2026 at 20:45 UTC 35a81cecf7d5be11dbf589575b1c93fdf224ba4a
2 files changed +29 -3
packages/react-reconciler/src/ReactFiberGestureScheduler.js
+21 -2
@@ -11,9 +11,19 @@ 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 +import type {Lane} from './ReactFiberLane';
15
15 -import {GestureLane, markRootFinished, NoLane, NoLanes} from './ReactFiberLane';
16 -import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
16 +import {
17 + GestureLane,
18 + markRootEntangled,
19 + markRootFinished,
20 + NoLane,
21 + NoLanes,
22 +} from './ReactFiberLane';
23 +import {
24 + ensureRootIsScheduled,
25 + requestTransitionLane,
26 +} from './ReactFiberRootScheduler';
27 import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
28 import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';
29
@@ -26,6 +36,7 @@ export type ScheduledGesture = {
36 types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
37 running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
38 committing: boolean, // If the gesture was released in a committed state and should actually commit.
39 + revertLane: Lane, // The Lane that we'll use to schedule the revert.
40 prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
41 next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
42 };
@@ -54,6 +65,7 @@ export function scheduleGesture(
65 types: null,
66 running: null,
67 committing: false,
68 + revertLane: NoLane, // Starts uninitialized.
69 prev: prev,
70 next: null,
71 };
@@ -119,6 +131,13 @@ export function cancelScheduledGesture(
131 root: FiberRoot,
132 gesture: ScheduledGesture,
133 ): void {
134 + // Entangle any Transitions started in this event with the revertLane of the gesture
135 + // so that we commit them all together.
136 + if (gesture.revertLane !== NoLane) {
137 + const entangledLanes = gesture.revertLane | requestTransitionLane(null);
138 + markRootEntangled(root, entangledLanes);
139 + }
140 +
141 gesture.count--;
142 if (gesture.count === 0) {
143 // If the end state is closer to the end than the beginning then we commit into the
packages/react-reconciler/src/ReactFiberHooks.js
+8 -1
@@ -3792,7 +3792,14 @@ function dispatchOptimisticSetState<S, A>(
3792 if (provider !== null) {
3793 // If this was a gesture, ensure we have a scheduled gesture and that
3794 // we associate this update with this specific gesture instance.
3795 - update.gesture = scheduleGesture(root, provider);
3795 + const gesture = (update.gesture = scheduleGesture(root, provider));
3796 + // Ensure the gesture always uses the same revert lane. This can happen for
3797 + // two startGestureTransition calls to the same provider in different events.
3798 + if (gesture.revertLane === NoLane) {
3799 + gesture.revertLane = update.revertLane;
3800 + } else {
3801 + update.revertLane = gesture.revertLane;
3802 + }
3803 }
3804 }
3805 }