@samitouri / QOS-React-1 / commits / a49952b303

Properly clean up gesture Animations (#35559)

Follow up to #35337. During a gesture, we always cancel the original animation and create a new one that we control. That's the one we need to add to the set that needs to be cancelled. Otherwise future gestures hang. An unfortunate consequence is that any custom ones that you start e.g. with #35556 or through other means aren't automatically cleaned up (in fact there's not even a clean up callback yet). This can lead these to freeze the whole UI afterwards. It would be really good to get this fixed in browsers instead so we can revert #35337.

Sebastian Markbåge committed Jan 19, 2026 at 19:26 UTC a49952b303a646c17f4d667956639300840a3e5b
1 file changed +10 -3
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+10 -3
@@ -2372,6 +2372,7 @@ function animateGesture(
2372 targetElement: Element,
2373 pseudoElement: string,
2374 timeline: GestureTimeline,
2375 + viewTransitionAnimations: Array<Animation>,
2376 customTimelineCleanup: Array<() => void>,
2377 rangeStart: number,
2378 rangeEnd: number,
@@ -2464,7 +2465,7 @@ function animateGesture(
2465 if (timeline instanceof AnimationTimeline) {
2466 // Native Timeline
2467 // $FlowFixMe[incompatible-call]
2467 - targetElement.animate(keyframes, {
2468 + const animation = targetElement.animate(keyframes, {
2469 pseudoElement: pseudoElement,
2470 // Set the timeline to the current gesture timeline to drive the updates.
2471 timeline: timeline,
@@ -2482,6 +2483,7 @@ function animateGesture(
2483 rangeStart: (reverse ? rangeEnd : rangeStart) + '%',
2484 rangeEnd: (reverse ? rangeStart : rangeEnd) + '%',
2485 });
2486 + viewTransitionAnimations.push(animation);
2487 } else {
2488 // Custom Timeline
2489 // $FlowFixMe[incompatible-call]
@@ -2554,8 +2556,10 @@ export function startGestureTransition(
2556 // $FlowFixMe
2557 const pseudoElement: ?string = effect.pseudoElement;
2558 if (pseudoElement == null) {
2557 - } else if (pseudoElement.startsWith('::view-transition')) {
2558 - viewTransitionAnimations.push(animations[i]);
2559 + } else if (
2560 + pseudoElement.startsWith('::view-transition') &&
2561 + effect.target === documentElement
2562 + ) {
2563 const timing = effect.getTiming();
2564 const duration =
2565 // $FlowFixMe[prop-missing]
@@ -2648,6 +2652,7 @@ export function startGestureTransition(
2652 effect.target,
2653 pseudoElement,
2654 timeline,
2655 + viewTransitionAnimations,
2656 customTimelineCleanup,
2657 adjustedRangeStart,
2658 adjustedRangeEnd,
@@ -2675,6 +2680,7 @@ export function startGestureTransition(
2680 effect.target,
2681 pseudoElementName,
2682 timeline,
2683 + viewTransitionAnimations,
2684 customTimelineCleanup,
2685 rangeStart,
2686 rangeEnd,
@@ -2696,6 +2702,7 @@ export function startGestureTransition(
2702 duration: 1,
2703 });
2704 blockingAnim.pause();
2705 + viewTransitionAnimations.push(blockingAnim);
2706 animateCallback();
2707 };
2708 // In Chrome, "new" animations are not ready in the ready callback. We have to wait