@samitouri / QOS-React-1 / commits / 61331f3c9e

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();