From 63d664b220b1587da0f3b4ced895456f3d8320da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 14 May 2025 17:50:56 -0400 Subject: [PATCH 1/2] 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. --- .../src/ReactFiberCommitWork.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 03d78545d4ea5..e21271f3b3eea 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -210,6 +210,7 @@ import { TransitionRoot, TransitionTracingMarker, } from './ReactFiberTracingMarkerComponent'; +import {getViewTransitionClassName} from './ReactFiberViewTransitionComponent'; import { commitHookLayoutEffects, commitHookLayoutUnmountEffects, @@ -303,6 +304,7 @@ export let shouldFireAfterActiveInstanceBlur: boolean = false; // Used during the commit phase to track whether a parent ViewTransition component // might have been affected by any mutations / relayouts below. let viewTransitionContextChanged: boolean = false; +let inUpdateViewTransition: boolean = false; let rootViewTransitionAffected: boolean = false; function isHydratingParent(current: Fiber, finishedWork: Fiber): boolean { @@ -1937,6 +1939,7 @@ export function commitMutationEffects( inProgressRoot = root; rootViewTransitionAffected = false; + inUpdateViewTransition = false; resetComponentEffectTimers(); @@ -2299,7 +2302,7 @@ function commitMutationEffectsOnFiber( recursivelyTraverseMutationEffects(root, finishedWork, lanes); commitReconciliationEffects(finishedWork, lanes); } - if (viewTransitionMutationContext) { + if (viewTransitionMutationContext && inUpdateViewTransition) { // A Portal doesn't necessarily exist within the context of this subtree. // Ideally we would track which React ViewTransition component nests the container // but that's costly. Instead, we treat each Portal as if it's a new React root. @@ -2534,11 +2537,16 @@ function commitMutationEffectsOnFiber( } } const prevMutationContext = pushMutationContext(); - recursivelyTraverseMutationEffects(root, finishedWork, lanes); - commitReconciliationEffects(finishedWork, lanes); + const prevUpdate = inUpdateViewTransition; const isViewTransitionEligible = enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes); + const props = finishedWork.memoizedProps; + inUpdateViewTransition = + isViewTransitionEligible && + getViewTransitionClassName(props.default, props.update) !== 'none'; + recursivelyTraverseMutationEffects(root, finishedWork, lanes); + commitReconciliationEffects(finishedWork, lanes); if (isViewTransitionEligible) { if (current === null) { // This is a new mount. We should have handled this as part of the @@ -2551,6 +2559,7 @@ function commitMutationEffectsOnFiber( finishedWork.flags |= Update; } } + inUpdateViewTransition = prevUpdate; popMutationContext(prevMutationContext); break; } @@ -2763,6 +2772,8 @@ function commitAfterMutationEffectsOnFiber( // Ideally we would track which React ViewTransition component nests the container // but that's costly. Instead, we treat each Portal as if it's a new React root. // Therefore any leaked resize of a child could affect the root so the root should animate. + // We only do this if the Portal is inside a ViewTransition and it is not disabled + // with update="none". Otherwise the Portal is considered not animating. rootViewTransitionAffected = true; } viewTransitionContextChanged = prevContextChanged; From 96eb84e493c4ff2c280990659057164c0f16bbb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 14 May 2025 17:52:41 -0400 Subject: [PATCH 2/2] Claim the useId name space for every auto named ViewTransition (#33200) This is a partial revert of #33094. It's true that we don't need the server and client ViewTransition names to line up. However the server does need to be able to generate deterministic names for itself. The cheapest way to do that is using the useId algorithm. When it's used by the server, the client needs to also materialize an ID even if it doesn't use it. --- .../src/ReactFiberBeginWork.js | 6 ++++++ packages/react-server/src/ReactFizzServer.js | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 975313a99f8b1..2e76e5188ec5f 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3543,6 +3543,12 @@ function updateViewTransition( current === null ? ViewTransitionNamedMount | ViewTransitionNamedStatic : ViewTransitionNamedStatic; + } else { + // The server may have used useId to auto-assign a generated name for this boundary. + // We push a materialization to ensure child ids line up with the server. + if (getIsHydrating()) { + pushMaterializedTreeId(workInProgress); + } } if (__DEV__) { // $FlowFixMe[prop-missing] diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index f86dc8c9ce020..4e8f4f86fdc7d 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -2273,7 +2273,23 @@ function renderViewTransition( ) { const prevKeyPath = task.keyPath; task.keyPath = keyPath; - renderNodeDestructive(request, task, props.children, -1); + if (props.name != null && props.name !== 'auto') { + renderNodeDestructive(request, task, props.children, -1); + } else { + // This will be auto-assigned a name which claims a "useId" slot. + // This component materialized an id. We treat this as its own level, with + // a single "child" slot. + const prevTreeContext = task.treeContext; + const totalChildren = 1; + const index = 0; + // Modify the id context. Because we'll need to reset this if something + // suspends or errors, we'll use the non-destructive render path. + task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index); + renderNode(request, task, props.children, -1); + // Like the other contexts, this does not need to be in a finally block + // because renderNode takes care of unwinding the stack. + task.treeContext = prevTreeContext; + } task.keyPath = prevKeyPath; }