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;