Repository: facebook/react
Pull request: Entangle Gesture revert commit with the corresponding Action commit

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.


Changed files:
filename: packages/react-reconciler/src/ReactFiberGestureScheduler.js
patch: @@ -11,9 +11,19 @@ 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 type {Lane} from './ReactFiberLane';
 
-import {GestureLane, markRootFinished, NoLane, NoLanes} from './ReactFiberLane';
-import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
+import {
+  GestureLane,
+  markRootEntangled,
+  markRootFinished,
+  NoLane,
+  NoLanes,
+} from './ReactFiberLane';
+import {
+  ensureRootIsScheduled,
+  requestTransitionLane,
+} from './ReactFiberRootScheduler';
 import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
 import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';
 
@@ -26,6 +36,7 @@ export type ScheduledGesture = {
   types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
   running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
   committing: boolean, // If the gesture was released in a committed state and should actually commit.
+  revertLane: Lane, // The Lane that we'll use to schedule the revert.
   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.
 };
@@ -54,6 +65,7 @@ export function scheduleGesture(
     types: null,
     running: null,
     committing: false,
+    revertLane: NoLane, // Starts uninitialized.
     prev: prev,
     next: null,
   };
@@ -119,6 +131,13 @@ export function cancelScheduledGesture(
   root: FiberRoot,
   gesture: ScheduledGesture,
 ): void {
+  // Entangle any Transitions started in this event with the revertLane of the gesture
+  // so that we commit them all together.
+  if (gesture.revertLane !== NoLane) {
+    const entangledLanes = gesture.revertLane | requestTransitionLane(null);
+    markRootEntangled(root, entangledLanes);
+  }
+
   gesture.count--;
   if (gesture.count === 0) {
     // If the end state is closer to the end than the beginning then we commit into the

filename: packages/react-reconciler/src/ReactFiberHooks.js
patch: @@ -3792,7 +3792,14 @@ function dispatchOptimisticSetState<S, A>(
         if (provider !== null) {
           // If this was a gesture, ensure we have a scheduled gesture and that
           // we associate this update with this specific gesture instance.
-          update.gesture = scheduleGesture(root, provider);
+          const gesture = (update.gesture = scheduleGesture(root, provider));
+          // Ensure the gesture always uses the same revert lane. This can happen for
+          // two startGestureTransition calls to the same provider in different events.
+          if (gesture.revertLane === NoLane) {
+            gesture.revertLane = update.revertLane;
+          } else {
+            update.revertLane = gesture.revertLane;
+          }
         }
       }
     }