@samitouri / QOS-React / commits / 63d664b220

Don't consider Portals animating unless they're wrapped in a ViewTransition (#33191)

And that doesn't disable with `update="none"`. The principle here is that we want the content of a Portal to animate if other things are animating with it but if other things aren't animating then we don't.

Sebastian Markbåge committed May 14, 2025 at 17:50 UTC 63d664b220b1587da0f3b4ced895456f3d8320da
1 file changed +14 -3
packages/react-reconciler/src/ReactFiberCommitWork.js
+14 -3
@@ -210,6 +210,7 @@ import {
210 TransitionRoot,
211 TransitionTracingMarker,
212 } from './ReactFiberTracingMarkerComponent';
213 +import {getViewTransitionClassName} from './ReactFiberViewTransitionComponent';
214 import {
215 commitHookLayoutEffects,
216 commitHookLayoutUnmountEffects,
@@ -303,6 +304,7 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false;
304 // Used during the commit phase to track whether a parent ViewTransition component
305 // might have been affected by any mutations / relayouts below.
306 let viewTransitionContextChanged: boolean = false;
307 +let inUpdateViewTransition: boolean = false;
308 let rootViewTransitionAffected: boolean = false;
309
310 function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean {
@@ -1937,6 +1939,7 @@ export function commitMutationEffects(
1939 inProgressRoot = root;
1940
1941 rootViewTransitionAffected = false;
1942 + inUpdateViewTransition = false;
1943
1944 resetComponentEffectTimers();
1945
@@ -2299,7 +2302,7 @@ function commitMutationEffectsOnFiber(
2302 recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2303 commitReconciliationEffects(finishedWork, lanes);
2304 }
2302 - if (viewTransitionMutationContext) {
2305 + if (viewTransitionMutationContext && inUpdateViewTransition) {
2306 // A Portal doesn't necessarily exist within the context of this subtree.
2307 // Ideally we would track which React ViewTransition component nests the container
2308 // but that's costly. Instead, we treat each Portal as if it's a new React root.
@@ -2534,11 +2537,16 @@ function commitMutationEffectsOnFiber(
2537 }
2538 }
2539 const prevMutationContext = pushMutationContext();
2537 - recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2538 - commitReconciliationEffects(finishedWork, lanes);
2540 + const prevUpdate = inUpdateViewTransition;
2541 const isViewTransitionEligible =
2542 enableViewTransition &&
2543 includesOnlyViewTransitionEligibleLanes(lanes);
2544 + const props = finishedWork.memoizedProps;
2545 + inUpdateViewTransition =
2546 + isViewTransitionEligible &&
2547 + getViewTransitionClassName(props.default, props.update) !== 'none';
2548 + recursivelyTraverseMutationEffects(root, finishedWork, lanes);
2549 + commitReconciliationEffects(finishedWork, lanes);
2550 if (isViewTransitionEligible) {
2551 if (current === null) {
2552 // This is a new mount. We should have handled this as part of the
@@ -2551,6 +2559,7 @@ function commitMutationEffectsOnFiber(
2559 finishedWork.flags |= Update;
2560 }
2561 }
2562 + inUpdateViewTransition = prevUpdate;
2563 popMutationContext(prevMutationContext);
2564 break;
2565 }
@@ -2763,6 +2772,8 @@ function commitAfterMutationEffectsOnFiber(
2772 // Ideally we would track which React ViewTransition component nests the container
2773 // but that's costly. Instead, we treat each Portal as if it's a new React root.
2774 // Therefore any leaked resize of a child could affect the root so the root should animate.
2775 + // We only do this if the Portal is inside a ViewTransition and it is not disabled
2776 + // with update="none". Otherwise the Portal is considered not animating.
2777 rootViewTransitionAffected = true;
2778 }
2779 viewTransitionContextChanged = prevContextChanged;