Fix ViewTransition crash in Mobile Safari
Project: facebook/react | Language: JavaScript | Concepts: error-handling | Difficulty: mid
Link: https://github.com/react/react/pull/35337
The problem
On Mobile Safari, using the getAnimations() API to cancel view transition animations was causing a crash. This affected React's handling of view transitions, leading to instability in apps using this feature on iOS devices.
Why it's non-trivial
A naive fix might simply try/catch around getAnimations() or ignore the error, but this would not guarantee that all relevant animations are properly cancelled. The timing and scope of animation creation and cancellation are subtle, and not all animations are discoverable via getAnimations() due to browser bugs or timing issues.
The approach
Old approach (removed):
Previously, the code used a helper to fetch and cancel all relevant animations via getAnimations({subtree: true}):
-function cancelAllViewTransitionAnimations(scope: Element) {
- // In Safari, we need to manually cancel all manually start animations
- // or it'll block or interfer with future transitions.
- // $FlowFixMe[prop-missing]
- const animations = scope.getAnimations({subtree: true});
- for (let i = 0; i < animations.length; i++) {
- const anim = animations[i];
- const effect: KeyframeEffect = (anim.effect: any);
- // $FlowFixMe
- const pseudo: ?string = effect.pseudoElement;
- if (
- pseudo != null &&
- pseudo.startsWith('::view-transition') &&
- effect.target === scope
- ) {
- anim.cancel();
- }
- }
-}
This function is now deleted because calling getAnimations() was unsafe in Mobile Safari.
New approach:
Instead of querying for all animations at the end, the code now collects references to relevant animations as they are created, and cancels only those later.
- Collect animations as they are created:
In both
startViewTransitionandstartGestureTransition, an array is introduced to keep track of relevant animations:
- const viewTransitionAnimations: Array<Animation> = [];
As animations are discovered in the ready callback, they are pushed into this array:
```diff
+ viewTransitionAnimations.push(animation);
- Cancel only the collected animations:
When the transition is finished, instead of calling
cancelAllViewTransitionAnimations, the code iterates over the collected array and cancels each animation:
-
cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
-
for (let i = 0; i < viewTransitionAnimations.length; i++) { -
// In Safari, we need to manually cancel all manually started animations -
// or it'll block or interfer with future transitions. -
// We can't use getAnimations() due to #35336 so we collect them in an array. -
viewTransitionAnimations[i].cancel(); -
}
This ensures only the animations created and tracked by React are cancelled, avoiding the buggy getAnimations() call.
From the review thread
sebmarkbage: Not sure if it fixes it in all cases though if the staleness also happens to apply to any of these.
gaearon: Yea not sure, I guess let's try it and see if I can hit it again at some other codepath
sebmarkbage: One unfortunate consequence of this is that if you spawn custom Animations using the refs passed to
onExit,onEnter,onUpdate,onSharecallbacks. E.g. to do JS powered animations. If you do that then they're not automatically cancelled by this mechanism since they're started later. There's no signal coming from React for when to cancel the animation.
Takeaways
- Avoid relying on browser APIs that are known to be buggy or inconsistent across platforms; instead, track resources explicitly when possible.
- Collect references to dynamically created objects at creation time if you need to reliably clean them up later.
- When changing resource management strategies, consider side effects for extensibility and custom usage—automatic cleanup may not cover externally created resources.