@samitouri / QOS-React-2 / commits / 4028aaa50c

Commit the Gesture lane if a gesture ends closer to the target state (#35486)

Stacked on #35485. Before this PR, the `startGestureTransition` API would itself never commit its state. After the gesture releases it stops the animation in the next commit which just leaves the DOM tree in the original state. If there's an actual state change from the Action then that's committed as the new DOM tree. To avoid animating from the original state to the new state again, this is DOM without an animation. However, this means that you can't have the actual action committing be in a slightly different state and animate between the final gesture state and into the new action. Instead, we now actually keep the render tree around and commit it in the end. Basically we assume that if the Timeline was closer to the end then visually you're already there and we can commit into that state. Most of the time this will be at the actual end state when you release but if you have something else cancelling the gesture (e.g. `touchcancel`) it can still commit this state even though your gesture recognizer might not consider this an Action. I think this is ok and keeps it simple. When the gesture lane commits, it'll leave a Transition behind as work from the revert lanes on the Optimistic updates. This means that if you don't do anything in the Action this will cause another commit right after which reverts. This revert can animate the snap back. There's a few fixes needed in follow up PRs: - Fixed in #35487. ~To support unentangled Transitions we need to explicitly entangle the revert lane with the Action to avoid committing a revert followed by a forward instead of committing the forward entangled with the revert. This just works now since everything is entangled but won't work with #35392.~ - Fixed in #35510. ~This currently rerenders the gesture lane once before committing if it was already completed but blocked. We should be able to commit the already completed tree as is.~

Sebastian Markbåge committed Jan 15, 2026 at 20:43 UTC 4028aaa50c92f1a546cd1aeac421bfabd66bef68
6 files changed +271 -156
fixtures/view-transition/src/components/Page.js
+14 -11
@@ -171,17 +171,20 @@ export default function Page({url, navigate}) {
171 }}>
172 <h1>{!show ? 'A' + counter : 'B'}</h1>
173 </ViewTransition>
174 - {show ? (
175 - <div>
176 - {a}
177 - {b}
178 - </div>
179 - ) : (
180 - <div>
181 - {b}
182 - {a}
183 - </div>
184 - )}
174 + {
175 + // Using url instead of renderedUrl here lets us only update this on commit.
176 + url === '/?b' ? (
177 + <div>
178 + {a}
179 + {b}
180 + </div>
181 + ) : (
182 + <div>
183 + {b}
184 + {a}
185 + </div>
186 + )
187 + }
188 <ViewTransition>
189 {show ? (
190 <div>hello{exclamation}</div>
packages/react-reconciler/src/ReactFiberGestureScheduler.js
+82 -62
@@ -12,13 +12,10 @@ 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,
17 - includesBlockingLane,
18 - includesTransitionLane,
19 -} from './ReactFiberLane';
15 +import {GestureLane, markRootFinished, NoLane, NoLanes} from './ReactFiberLane';
16 import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
17 import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
18 +import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';
19
20 // This type keeps track of any scheduled or active gestures.
21 export type ScheduledGesture = {
@@ -28,6 +25,7 @@ export type ScheduledGesture = {
25 rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
26 types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
27 running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
28 + committing: boolean, // If the gesture was released in a committed state and should actually commit.
29 prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
30 next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
31 };
@@ -55,6 +53,7 @@ export function scheduleGesture(
53 rangeEnd: 100, // Uninitialized
54 types: null,
55 running: null,
56 + committing: false,
57 prev: prev,
58 next: null,
59 };
@@ -122,77 +121,98 @@ export function cancelScheduledGesture(
121 ): void {
122 gesture.count--;
123 if (gesture.count === 0) {
125 - // Delete the scheduled gesture from the pending queue.
126 - deleteScheduledGesture(root, gesture);
124 + // If the end state is closer to the end than the beginning then we commit into the
125 + // end state before reverting back (or applying a new Transition).
126 + // Otherwise we just revert back and don't commit.
127 + let shouldCommit: boolean;
128 + const finalOffset = getCurrentGestureOffset(gesture.provider);
129 + const rangeStart = gesture.rangeStart;
130 + const rangeEnd = gesture.rangeEnd;
131 + if (rangeStart < rangeEnd) {
132 + shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2;
133 + } else {
134 + shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2;
135 + }
136 // TODO: If we're currently rendering this gesture, we need to restart the render
137 // on a different gesture or cancel the render..
138 // TODO: We might want to pause the View Transition at this point since you should
139 // no longer be able to update the position of anything but it might be better to
140 // just commit the gesture state.
141 const runningTransition = gesture.running;
133 - if (runningTransition !== null) {
134 - const pendingLanesExcludingGestureLane = root.pendingLanes & ~GestureLane;
135 - if (
136 - includesBlockingLane(pendingLanesExcludingGestureLane) ||
137 - includesTransitionLane(pendingLanesExcludingGestureLane)
138 - ) {
139 - // If we have pending work we schedule the gesture to be stopped at the next commit.
140 - // This ensures that we don't snap back to the previous state until we have
141 - // had a chance to commit any resulting updates.
142 - const existing = root.stoppingGestures;
143 - if (existing !== null) {
144 - gesture.next = existing;
145 - existing.prev = gesture;
142 + if (runningTransition !== null && shouldCommit) {
143 + // If we are going to commit this gesture in its to state, we need to wait to
144 + // stop it until it commits. We should now schedule a render at the gesture
145 + // lane to actually commit it.
146 + gesture.committing = true;
147 + if (root.pendingGestures === gesture) {
148 + // Ping the root given the new state. This is similar to pingSuspendedRoot.
149 + // This will either schedule the gesture lane to be committed possibly from its current state.
150 + pingGestureRoot(root);
151 + }
152 + } else {
153 + // If we're not going to commit this gesture we can stop the View Transition
154 + // right away and delete the scheduled gesture from the pending queue.
155 + if (gesture.prev === null) {
156 + if (root.pendingGestures === gesture) {
157 + // This was the currently rendering gesture.
158 + root.pendingGestures = gesture.next;
159 + let remainingLanes = root.pendingLanes;
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 + remainingLanes &= ~GestureLane;
165 + }
166 + markRootFinished(
167 + root,
168 + GestureLane,
169 + remainingLanes,
170 + NoLane,
171 + NoLane,
172 + NoLanes,
173 + );
174 + // If we had a currently rendering gesture we need to now reset the gesture lane to
175 + // now render the next gesture or cancel if there's no more gestures in the queue.
176 + restartGestureRoot(root);
177 }
147 - root.stoppingGestures = gesture;
148 - } else {
178 gesture.running = null;
150 - // If there's no work scheduled so we can stop the View Transition right away.
151 - stopViewTransition(runningTransition);
179 + if (runningTransition !== null) {
180 + stopViewTransition(runningTransition);
181 + }
182 + } else {
183 + // This was not the current gesture so it doesn't affect the current render.
184 + gesture.prev.next = gesture.next;
185 + if (gesture.next !== null) {
186 + gesture.next.prev = gesture.prev;
187 + }
188 + gesture.prev = null;
189 + gesture.next = null;
190 }
191 }
192 }
193 }
194
157 -export function deleteScheduledGesture(
158 - root: FiberRoot,
159 - gesture: ScheduledGesture,
160 -): void {
161 - if (gesture.prev === null) {
162 - if (root.pendingGestures === gesture) {
163 - root.pendingGestures = gesture.next;
164 - if (root.pendingGestures === null) {
165 - // Gestures don't clear their lanes while the gesture is still active but it
166 - // might not be scheduled to do any more renders and so we shouldn't schedule
167 - // any more gesture lane work until a new gesture is scheduled.
168 - root.pendingLanes &= ~GestureLane;
169 - }
170 - }
171 - if (root.stoppingGestures === gesture) {
172 - // This should not really happen the way we use it now but just in case we start.
173 - root.stoppingGestures = gesture.next;
174 - }
175 - } else {
176 - gesture.prev.next = gesture.next;
177 - if (gesture.next !== null) {
178 - gesture.next.prev = gesture.prev;
195 +export function stopCommittedGesture(root: FiberRoot) {
196 + // The top was just committed. We can delete it from the queue
197 + // and stop its View Transition now.
198 + const committedGesture = root.pendingGestures;
199 + if (committedGesture !== null) {
200 + // Mark it as no longer committing and should no longer be included in rerenders.
201 + committedGesture.committing = false;
202 + const nextGesture = committedGesture.next;
203 + if (nextGesture === null) {
204 + // Gestures don't clear their lanes while the gesture is still active but it
205 + // might not be scheduled to do any more renders and so we shouldn't schedule
206 + // any more gesture lane work until a new gesture is scheduled.
207 + root.pendingLanes &= ~GestureLane;
208 + } else {
209 + nextGesture.prev = null;
210 }
180 - gesture.prev = null;
181 - gesture.next = null;
182 - }
183 -}
184 -
185 -export function stopCompletedGestures(root: FiberRoot) {
186 - let gesture = root.stoppingGestures;
187 - root.stoppingGestures = null;
188 - while (gesture !== null) {
189 - if (gesture.running !== null) {
190 - stopViewTransition(gesture.running);
191 - gesture.running = null;
211 + root.pendingGestures = nextGesture;
212 + const runningTransition = committedGesture.running;
213 + if (runningTransition !== null) {
214 + committedGesture.running = null;
215 + stopViewTransition(runningTransition);
216 }
193 - const nextGesture = gesture.next;
194 - gesture.next = null;
195 - gesture.prev = null;
196 - gesture = nextGesture;
217 }
218 }
packages/react-reconciler/src/ReactFiberHooks.js
+1 -1
@@ -1384,7 +1384,7 @@ function updateReducerImpl<S, A>(
1384 // ScheduledGesture.
1385 const scheduledGesture = update.gesture;
1386 if (scheduledGesture !== null) {
1387 - if (scheduledGesture.count === 0) {
1387 + if (scheduledGesture.count === 0 && !scheduledGesture.committing) {
1388 // This gesture has already been cancelled. We can clean up this update.
1389 update = update.next;
1390 continue;
packages/react-reconciler/src/ReactFiberRoot.js
-1
@@ -115,7 +115,6 @@ function FiberRootNode(
115
116 if (enableGestureTransition) {
117 this.pendingGestures = null;
118 - this.stoppingGestures = null;
118 this.gestureClone = null;
119 }
120
packages/react-reconciler/src/ReactFiberWorkLoop.js
+174 -80
@@ -398,10 +398,7 @@ import {
398 } from './ReactFiberRootScheduler';
399 import {getMaskedContext, getUnmaskedContext} from './ReactFiberLegacyContext';
400 import {logUncaughtError} from './ReactFiberErrorLogger';
401 -import {
402 - deleteScheduledGesture,
403 - stopCompletedGestures,
404 -} from './ReactFiberGestureScheduler';
401 +import {stopCommittedGesture} from './ReactFiberGestureScheduler';
402 import {claimQueuedTransitionTypes} from './ReactFiberTransitionTypes';
403
404 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
@@ -1407,7 +1404,7 @@ function finishConcurrentRender(
1404
1405 if (shouldForceFlushFallbacksInDEV()) {
1406 // We're inside an `act` scope. Commit immediately.
1410 - commitRoot(
1407 + completeRoot(
1408 root,
1409 finishedWork,
1410 lanes,
@@ -1417,6 +1414,7 @@ function finishConcurrentRender(
1414 workInProgressDeferredLane,
1415 workInProgressRootInterleavedUpdatedLanes,
1416 workInProgressSuspendedRetryLanes,
1417 + workInProgressRootDidSkipSuspendedSiblings,
1418 exitStatus,
1419 null,
1420 null,
@@ -1458,7 +1456,7 @@ function finishConcurrentRender(
1456 // run one after the other.
1457 pendingEffectsLanes = lanes;
1458 root.timeoutHandle = scheduleTimeout(
1461 - commitRootWhenReady.bind(
1459 + completeRootWhenReady.bind(
1460 null,
1461 root,
1462 finishedWork,
@@ -1480,7 +1478,7 @@ function finishConcurrentRender(
1478 return;
1479 }
1480 }
1483 - commitRootWhenReady(
1481 + completeRootWhenReady(
1482 root,
1483 finishedWork,
1484 workInProgressRootRecoverableErrors,
@@ -1499,7 +1497,7 @@ function finishConcurrentRender(
1497 }
1498 }
1499
1502 -function commitRootWhenReady(
1500 +function completeRootWhenReady(
1501 root: FiberRoot,
1502 finishedWork: Fiber,
1503 recoverableErrors: Array<CapturedValue<mixed>> | null,
@@ -1540,12 +1538,18 @@ function commitRootWhenReady(
1538 // This will also track any newly added or appearing ViewTransition
1539 // components for the purposes of forming pairs.
1540 accumulateSuspenseyCommit(finishedWork, lanes, suspendedState);
1543 - if (isViewTransitionEligible || isGestureTransition) {
1544 - // If we're stopping gestures we don't have to wait for any pending
1545 - // view transition. We'll stop it when we commit.
1546 - if (!enableGestureTransition || root.stoppingGestures === null) {
1547 - suspendOnActiveViewTransition(suspendedState, root.containerInfo);
1548 - }
1541 + if (
1542 + isViewTransitionEligible ||
1543 + (isGestureTransition &&
1544 + root.pendingGestures !== null &&
1545 + // If we're committing this gesture and it already has a View Transition
1546 + // running, then we don't have to wait for that gesture. We'll stop it
1547 + // when we commit.
1548 + (root.pendingGestures.running === null ||
1549 + !root.pendingGestures.committing))
1550 + ) {
1551 + // Wait for any pending View Transition (including gestures) to finish.
1552 + suspendOnActiveViewTransition(suspendedState, root.containerInfo);
1553 }
1554 // For timeouts we use the previous fallback commit for retries and
1555 // the start time of the transition for transitions. This offset
@@ -1572,7 +1576,7 @@ function commitRootWhenReady(
1576 // root again.
1577 pendingEffectsLanes = lanes;
1578 root.cancelPendingCommit = schedulePendingCommit(
1575 - commitRoot.bind(
1579 + completeRoot.bind(
1580 null,
1581 root,
1582 finishedWork,
@@ -1583,6 +1587,7 @@ function commitRootWhenReady(
1587 spawnedLane,
1588 updatedLanes,
1589 suspendedRetryLanes,
1590 + didSkipSuspendedSiblings,
1591 exitStatus,
1592 suspendedState,
1593 enableProfilerTimer
@@ -1599,7 +1604,7 @@ function commitRootWhenReady(
1604 }
1605
1606 // Otherwise, commit immediately.;
1602 - commitRoot(
1607 + completeRoot(
1608 root,
1609 finishedWork,
1610 lanes,
@@ -1609,6 +1614,7 @@ function commitRootWhenReady(
1614 spawnedLane,
1615 updatedLanes,
1616 suspendedRetryLanes,
1617 + didSkipSuspendedSiblings,
1618 exitStatus,
1619 suspendedState,
1620 suspendedCommitReason,
@@ -3420,7 +3426,7 @@ function unwindUnitOfWork(unitOfWork: Fiber, skipSiblings: boolean): void {
3426 workInProgress = null;
3427 }
3428
3423 -function commitRoot(
3429 +function completeRoot(
3430 root: FiberRoot,
3431 finishedWork: null | Fiber,
3432 lanes: Lanes,
@@ -3430,6 +3436,7 @@ function commitRoot(
3436 spawnedLane: Lane,
3437 updatedLanes: Lanes,
3438 suspendedRetryLanes: Lanes,
3439 + didSkipSuspendedSiblings: boolean,
3440 exitStatus: RootExitStatus,
3441 suspendedState: null | SuspendedState,
3442 suspendedCommitReason: SuspendedCommitReason, // Profiling-only
@@ -3496,9 +3503,9 @@ function commitRoot(
3503 markCommitStopped();
3504 }
3505 if (enableGestureTransition) {
3499 - // Stop any gestures that were completed and is now being reverted.
3500 - if (root.stoppingGestures !== null) {
3501 - stopCompletedGestures(root);
3506 + // Stop any gestures that were committed.
3507 + if (isGestureRender(lanes)) {
3508 + stopCommittedGesture(root);
3509 }
3510 }
3511 return;
@@ -3520,10 +3527,95 @@ function commitRoot(
3527 );
3528 }
3529
3530 + if (root === workInProgressRoot) {
3531 + // We can reset these now that they are finished.
3532 + workInProgressRoot = null;
3533 + workInProgress = null;
3534 + workInProgressRootRenderLanes = NoLanes;
3535 + } else {
3536 + // This indicates that the last root we worked on is not the same one that
3537 + // we're committing now. This most commonly happens when a suspended root
3538 + // times out.
3539 + }
3540 +
3541 + // workInProgressX might be overwritten, so we want
3542 + // to store it in pendingPassiveX until they get processed
3543 + // We need to pass this through as an argument to completeRoot
3544 + // because workInProgressX might have changed between
3545 + // the previous render and commit if we throttle the commit
3546 + // with setTimeout
3547 + pendingFinishedWork = finishedWork;
3548 + pendingEffectsRoot = root;
3549 + pendingEffectsLanes = lanes;
3550 + pendingPassiveTransitions = transitions;
3551 + pendingRecoverableErrors = recoverableErrors;
3552 + pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
3553 + if (enableProfilerTimer) {
3554 + pendingEffectsRenderEndTime = completedRenderEndTime;
3555 + pendingSuspendedCommitReason = suspendedCommitReason;
3556 + pendingDelayedCommitReason = IMMEDIATE_COMMIT;
3557 + pendingSuspendedViewTransitionReason = null;
3558 + }
3559 +
3560 + if (enableGestureTransition && isGestureRender(lanes)) {
3561 + const committingGesture = root.pendingGestures;
3562 + if (committingGesture !== null && !committingGesture.committing) {
3563 + // This gesture is not ready to commit yet. We'll mark it as suspended and
3564 + // start a gesture transition which isn't really a side-effect. Then later
3565 + // we might come back around to actually committing the root.
3566 + const didAttemptEntireTree = !didSkipSuspendedSiblings;
3567 + markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);
3568 + if (committingGesture.running === null) {
3569 + applyGestureOnRoot(
3570 + root,
3571 + finishedWork,
3572 + recoverableErrors,
3573 + suspendedState,
3574 + enableProfilerTimer
3575 + ? suspendedCommitReason === null
3576 + ? completedRenderEndTime
3577 + : commitStartTime
3578 + : 0,
3579 + );
3580 + } else {
3581 + // If we already have a gesture running, we don't update it in place
3582 + // even if we have a new tree. Instead we wait until we can commit.
3583 + }
3584 + return;
3585 + }
3586 + }
3587 +
3588 + // If we're not starting a gesture we now actually commit the root.
3589 + commitRoot(
3590 + root,
3591 + finishedWork,
3592 + lanes,
3593 + spawnedLane,
3594 + updatedLanes,
3595 + suspendedRetryLanes,
3596 + suspendedState,
3597 + suspendedCommitReason,
3598 + completedRenderEndTime,
3599 + );
3600 +}
3601 +
3602 +function commitRoot(
3603 + root: FiberRoot,
3604 + finishedWork: Fiber,
3605 + lanes: Lanes,
3606 + spawnedLane: Lane,
3607 + updatedLanes: Lanes,
3608 + suspendedRetryLanes: Lanes,
3609 + suspendedState: null | SuspendedState,
3610 + suspendedCommitReason: SuspendedCommitReason, // Profiling-only
3611 + completedRenderEndTime: number, // Profiling-only
3612 +) {
3613 // Check which lanes no longer have any work scheduled on them, and mark
3614 // those as finished.
3615 let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
3616
3617 + pendingEffectsRemainingLanes = remainingLanes;
3618 +
3619 // Make sure to account for lanes that were updated by a concurrent event
3620 // during the render phase; don't mark them as finished.
3621 const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
@@ -3553,53 +3645,6 @@ function commitRoot(
3645 // Reset this before firing side effects so we can detect recursive updates.
3646 didIncludeCommitPhaseUpdate = false;
3647
3556 - if (root === workInProgressRoot) {
3557 - // We can reset these now that they are finished.
3558 - workInProgressRoot = null;
3559 - workInProgress = null;
3560 - workInProgressRootRenderLanes = NoLanes;
3561 - } else {
3562 - // This indicates that the last root we worked on is not the same one that
3563 - // we're committing now. This most commonly happens when a suspended root
3564 - // times out.
3565 - }
3566 -
3567 - // workInProgressX might be overwritten, so we want
3568 - // to store it in pendingPassiveX until they get processed
3569 - // We need to pass this through as an argument to commitRoot
3570 - // because workInProgressX might have changed between
3571 - // the previous render and commit if we throttle the commit
3572 - // with setTimeout
3573 - pendingFinishedWork = finishedWork;
3574 - pendingEffectsRoot = root;
3575 - pendingEffectsLanes = lanes;
3576 - pendingEffectsRemainingLanes = remainingLanes;
3577 - pendingPassiveTransitions = transitions;
3578 - pendingRecoverableErrors = recoverableErrors;
3579 - pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;
3580 - if (enableProfilerTimer) {
3581 - pendingEffectsRenderEndTime = completedRenderEndTime;
3582 - pendingSuspendedCommitReason = suspendedCommitReason;
3583 - pendingDelayedCommitReason = IMMEDIATE_COMMIT;
3584 - pendingSuspendedViewTransitionReason = null;
3585 - }
3586 -
3587 - if (enableGestureTransition && isGestureRender(lanes)) {
3588 - // This is a special kind of render that doesn't commit regular effects.
3589 - commitGestureOnRoot(
3590 - root,
3591 - finishedWork,
3592 - recoverableErrors,
3593 - suspendedState,
3594 - enableProfilerTimer
3595 - ? suspendedCommitReason === null
3596 - ? completedRenderEndTime
3597 - : commitStartTime
3598 - : 0,
3599 - );
3600 - return;
3601 - }
3602 -
3648 // If there are pending passive effects, schedule a callback to process them.
3649 // Do this as early as possible, so it is queued before anything else that
3650 // might get scheduled in the commit phase. (See #16714.)
@@ -3712,20 +3757,19 @@ function commitRoot(
3757 }
3758 }
3759
3715 - let willStartViewTransition = shouldStartViewTransition;
3760 if (enableGestureTransition) {
3717 - // Stop any gestures that were completed and is now being committed.
3718 - if (root.stoppingGestures !== null) {
3719 - stopCompletedGestures(root);
3720 - // If we are in the process of stopping some gesture we shouldn't start
3721 - // a View Transition because that would start from the previous state to
3722 - // the next state.
3723 - willStartViewTransition = false;
3761 + // Stop any gestures that were committed.
3762 + if (isGestureRender(lanes)) {
3763 + stopCommittedGesture(root);
3764 + // Note that shouldStartViewTransition should always be false here because
3765 + // committing a gesture never starts a new View Transition itself since it's
3766 + // not a View Transition eligible lane. Only follow up Transition commits can
3767 + // cause animate.
3768 }
3769 }
3770
3771 pendingEffectsStatus = PENDING_MUTATION_PHASE;
3728 - if (enableViewTransition && willStartViewTransition) {
3772 + if (enableViewTransition && shouldStartViewTransition) {
3773 if (enableProfilerTimer && enableComponentPerformanceTrack) {
3774 startAnimating(lanes);
3775 }
@@ -4155,7 +4199,7 @@ function flushSpawnedWork(): void {
4199 flushPendingEffects();
4200 }
4201
4158 - // Always call this before exiting `commitRoot`, to ensure that any
4202 + // Always call this before exiting `completeRoot`, to ensure that any
4203 // additional work on this root is scheduled.
4204 ensureRootIsScheduled(root);
4205
@@ -4244,7 +4288,7 @@ function flushSpawnedWork(): void {
4288 }
4289 }
4290
4247 -function commitGestureOnRoot(
4291 +function applyGestureOnRoot(
4292 root: FiberRoot,
4293 finishedWork: Fiber,
4294 recoverableErrors: null | Array<CapturedValue<mixed>>,
@@ -4259,7 +4303,6 @@ function commitGestureOnRoot(
4303 ensureRootIsScheduled(root);
4304 return;
4305 }
4262 - deleteScheduledGesture(root, finishedGesture);
4306
4307 if (enableProfilerTimer && enableComponentPerformanceTrack) {
4308 startAnimating(pendingEffectsLanes);
@@ -4468,7 +4511,7 @@ function flushPassiveEffects(): boolean {
4511 // flushPassiveEffectsImpl
4512 const root = pendingEffectsRoot;
4513 // Cache and clear the remaining lanes flag; it must be reset since this
4471 - // method can be called from various places, not always from commitRoot
4514 + // method can be called from various places, not always from completeRoot
4515 // where the remaining lanes are known
4516 const remainingLanes = pendingEffectsRemainingLanes;
4517 pendingEffectsRemainingLanes = NoLanes;
@@ -4856,6 +4899,57 @@ function pingSuspendedRoot(
4899 ensureRootIsScheduled(root);
4900 }
4901
4902 +export function pingGestureRoot(root: FiberRoot): void {
4903 + const gesture = root.pendingGestures;
4904 + if (gesture === null) {
4905 + return;
4906 + }
4907 + if (
4908 + root.cancelPendingCommit !== null &&
4909 + isGestureRender(pendingEffectsLanes)
4910 + ) {
4911 + // We have a suspended commit which we'll discard and rerender.
4912 + // TODO: Just use this commit since it's ready to go.
4913 + const cancelPendingCommit = root.cancelPendingCommit;
4914 + if (cancelPendingCommit !== null) {
4915 + root.cancelPendingCommit = null;
4916 + cancelPendingCommit();
4917 + }
4918 + }
4919 + // Ping it for rerender and commit.
4920 + markRootPinged(root, GestureLane);
4921 + ensureRootIsScheduled(root);
4922 +}
4923 +
4924 +export function restartGestureRoot(root: FiberRoot): void {
4925 + if (
4926 + workInProgressRoot === root &&
4927 + isGestureRender(workInProgressRootRenderLanes)
4928 + ) {
4929 + // The current render was a gesture but it's now defunct. We need to restart the render.
4930 + if ((executionContext & RenderContext) === NoContext) {
4931 + prepareFreshStack(root, NoLanes);
4932 + } else {
4933 + // TODO: Throw interruption when supported again.
4934 + }
4935 + } else if (
4936 + root.cancelPendingCommit !== null &&
4937 + isGestureRender(pendingEffectsLanes)
4938 + ) {
4939 + // We have a suspended commit which we'll discard.
4940 + const cancelPendingCommit = root.cancelPendingCommit;
4941 + if (cancelPendingCommit !== null) {
4942 + root.cancelPendingCommit = null;
4943 + cancelPendingCommit();
4944 + }
4945 + }
4946 + if (root.pendingGestures !== null) {
4947 + // We still have gestures to work on. Let's schedule a restart.
4948 + markRootPinged(root, GestureLane);
4949 + }
4950 + ensureRootIsScheduled(root);
4951 +}
4952 +
4953 function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
4954 // The boundary fiber (a Suspense component or SuspenseList component)
4955 // previously was rendered in its fallback state. One of the promises that
packages/react-reconciler/src/ReactInternalTypes.js
-1
@@ -292,7 +292,6 @@ type BaseFiberRootProperties = {
292 transitionTypes: null | TransitionTypes, // TODO: Make this a LaneMap.
293 // enableGestureTransition only
294 pendingGestures: null | ScheduledGesture,
295 - stoppingGestures: null | ScheduledGesture,
295 gestureClone: null | Instance,
296 };
297