Fix ViewTransition crash in Mobile Safari (#35337)
Speculative fix to https://github.com/facebook/react/issues/35336 written by Claude. I have verified that applying a similar patch locally to the repro from #35336 does fix the crash. I'm not familiar enough with the underlying APIs to tell whether the fix is correct or sufficient.
dan committed
Dec 10, 2025 at 03:35 UTC
61331f3c9e9ea93d866273567d38e23ef4bc4c5b
1 file changed
+17
-22
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+17
-22
@@ -1996,26 +1996,6 @@ export function hasInstanceAffectedParent(
1996
return oldRect.height !== newRect.height || oldRect.width !== newRect.width;
1997
}
1998
1999
-function cancelAllViewTransitionAnimations(scope: Element) {
2000
- // In Safari, we need to manually cancel all manually start animations
2001
- // or it'll block or interfer with future transitions.
2002
- // $FlowFixMe[prop-missing]
2003
- const animations = scope.getAnimations({subtree: true});
2004
- for (let i = 0; i < animations.length; i++) {
2005
- const anim = animations[i];
2006
- const effect: KeyframeEffect = (anim.effect: any);
2007
- // $FlowFixMe
2008
- const pseudo: ?string = effect.pseudoElement;
2009
- if (
2010
- pseudo != null &&
2011
- pseudo.startsWith('::view-transition') &&
2012
- effect.target === scope
2013
- ) {
2014
- anim.cancel();
2015
- }
2016
- }
2017
-}
2018
-
1999
// How long to wait for new fonts to load before just committing anyway.
2000
// This freezes the screen. It needs to be short enough that it doesn't cause too much of
2001
// an issue when it's a new load and slow, yet long enough that you have a chance to load
@@ -2210,6 +2190,8 @@ export function startViewTransition(
2190
// $FlowFixMe[prop-missing]
2191
ownerDocument.__reactViewTransition = transition;
2192
2193
+ const viewTransitionAnimations: Array<Animation> = [];
2194
+
2195
const readyCallback = () => {
2196
const documentElement: Element = (ownerDocument.documentElement: any);
2197
// Loop through all View Transition Animations.
@@ -2224,6 +2206,7 @@ export function startViewTransition(
2206
pseudoElement != null &&
2207
pseudoElement.startsWith('::view-transition')
2208
) {
2209
+ viewTransitionAnimations.push(animation);
2210
const keyframes = effect.getKeyframes();
2211
// Next, we're going to try to optimize this animation in case the auto-generated
2212
// width/height keyframes are unnecessary.
@@ -2315,7 +2298,12 @@ export function startViewTransition(
2298
};
2299
transition.ready.then(readyCallback, handleError);
2300
transition.finished.finally(() => {
2318
- cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
2301
+ for (let i = 0; i < viewTransitionAnimations.length; i++) {
2302
+ // In Safari, we need to manually cancel all manually started animations
2303
+ // or it'll block or interfer with future transitions.
2304
+ // We can't use getAnimations() due to #35336 so we collect them in an array.
2305
+ viewTransitionAnimations[i].cancel();
2306
+ }
2307
// $FlowFixMe[prop-missing]
2308
if (ownerDocument.__reactViewTransition === transition) {
2309
// $FlowFixMe[prop-missing]
@@ -2549,6 +2537,7 @@ export function startGestureTransition(
2537
// $FlowFixMe[prop-missing]
2538
ownerDocument.__reactViewTransition = transition;
2539
const customTimelineCleanup: Array<() => void> = []; // Cleanup Animations started in a CustomTimeline
2540
+ const viewTransitionAnimations: Array<Animation> = [];
2541
const readyCallback = () => {
2542
const documentElement: Element = (ownerDocument.documentElement: any);
2543
// Loop through all View Transition Animations.
@@ -2566,6 +2555,7 @@ export function startGestureTransition(
2555
const pseudoElement: ?string = effect.pseudoElement;
2556
if (pseudoElement == null) {
2557
} else if (pseudoElement.startsWith('::view-transition')) {
2558
+ viewTransitionAnimations.push(animations[i]);
2559
const timing = effect.getTiming();
2560
const duration =
2561
// $FlowFixMe[prop-missing]
@@ -2743,7 +2733,12 @@ export function startGestureTransition(
2733
};
2734
transition.ready.then(readyForAnimations, handleError);
2735
transition.finished.finally(() => {
2746
- cancelAllViewTransitionAnimations((ownerDocument.documentElement: any));
2736
+ for (let i = 0; i < viewTransitionAnimations.length; i++) {
2737
+ // In Safari, we need to manually cancel all manually started animations
2738
+ // or it'll block or interfer with future transitions.
2739
+ // We can't use getAnimations() due to #35336 so we collect them in an array.
2740
+ viewTransitionAnimations[i].cancel();
2741
+ }
2742
for (let i = 0; i < customTimelineCleanup.length; i++) {
2743
const cleanup = customTimelineCleanup[i];
2744
cleanup();