From 05616d2f6120824003239c9d7b318daeb5c03c0c Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 13 Mar 2025 20:36:22 -0400 Subject: [PATCH 01/14] Gate flushGestureMutations and flushGestureAnimations --- packages/react-reconciler/src/ReactFiberWorkLoop.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index d7fe0893633fc..380fa20c3d1c1 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -3927,6 +3927,9 @@ function commitGestureOnRoot( } function flushGestureMutations(): void { + if (!enableSwipeTransition) { + return; + } if (pendingEffectsStatus !== PENDING_GESTURE_MUTATION_PHASE) { return; } @@ -3953,6 +3956,9 @@ function flushGestureMutations(): void { } function flushGestureAnimations(): void { + if (!enableSwipeTransition) { + return; + } // If we get canceled before we start we might not have applied // mutations yet. We need to apply them first. flushGestureMutations(); From 2296cfc6d7ce5b02d608d52e29d4fb8e98eab7c0 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 12 Mar 2025 17:31:19 -0400 Subject: [PATCH 02/14] Merge restoreEnterViewTransitions and restoreExitViewTransitions --- .../src/ReactFiberCommitViewTransitions.js | 35 +++++-------------- .../src/ReactFiberCommitWork.js | 9 +++-- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 22b9d11ac67d2..5dcf1498ed18a 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -514,37 +514,20 @@ function restorePairedViewTransitions(parent: Fiber): void { } } -export function restoreEnterViewTransitions(placement: Fiber): void { - if (placement.tag === ViewTransitionComponent) { - const instance: ViewTransitionState = placement.stateNode; - instance.paired = null; - restoreViewTransitionOnHostInstances(placement.child, false); - restorePairedViewTransitions(placement); - } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) { - let child = placement.child; - while (child !== null) { - restoreEnterViewTransitions(child); - child = child.sibling; - } - } else { - restorePairedViewTransitions(placement); - } -} - -export function restoreExitViewTransitions(deletion: Fiber): void { - if (deletion.tag === ViewTransitionComponent) { - const instance: ViewTransitionState = deletion.stateNode; +export function restoreEnterOrExitViewTransitions(fiber: Fiber): void { + if (fiber.tag === ViewTransitionComponent) { + const instance: ViewTransitionState = fiber.stateNode; instance.paired = null; - restoreViewTransitionOnHostInstances(deletion.child, false); - restorePairedViewTransitions(deletion); - } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) { - let child = deletion.child; + restoreViewTransitionOnHostInstances(fiber.child, false); + restorePairedViewTransitions(fiber); + } else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) { + let child = fiber.child; while (child !== null) { - restoreExitViewTransitions(child); + restoreEnterOrExitViewTransitions(child); child = child.sibling; } } else { - restorePairedViewTransitions(deletion); + restorePairedViewTransitions(fiber); } } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 8b82e6a7e713c..bcf7fc188ee46 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -246,8 +246,7 @@ import { commitExitViewTransitions, commitBeforeUpdateViewTransition, commitNestedViewTransitions, - restoreEnterViewTransitions, - restoreExitViewTransitions, + restoreEnterOrExitViewTransitions, restoreUpdateViewTransition, restoreNestedViewTransitions, measureUpdateViewTransition, @@ -3228,7 +3227,7 @@ function commitPassiveMountOnFiber( // This was a new mount. This means we could've triggered an enter animation on // the content. Restore the view transitions if there were any assigned in the // snapshot phase. - restoreEnterViewTransitions(finishedWork); + restoreEnterOrExitViewTransitions(finishedWork); } // When updating this function, also update reconnectPassiveEffects, which does @@ -3529,7 +3528,7 @@ function commitPassiveMountOnFiber( // Content is now hidden but wasn't before. This means we could've // triggered an exit animation on the content. Restore the view // transitions if there were any assigned in the snapshot phase. - restoreExitViewTransitions(current); + restoreEnterOrExitViewTransitions(current); } if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. @@ -3576,7 +3575,7 @@ function commitPassiveMountOnFiber( // Content is now visible but wasn't before. This means we could've // triggered an enter animation on the content. Restore the view // transitions if there were any assigned in the snapshot phase. - restoreEnterViewTransitions(finishedWork); + restoreEnterOrExitViewTransitions(finishedWork); } if (instance._visibility & OffscreenPassiveEffectsConnected) { // The effects are currently connected. Update them. From e4e800143f0e487dda2fc2c7a234a195fa978479 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 10 Mar 2025 17:15:24 -0400 Subject: [PATCH 03/14] Store Cloned Host Instances temporarily on the View Transition We need to be able to refer to these to measure and apply names to them in future passes. --- packages/react-reconciler/src/ReactFiber.js | 1 + .../src/ReactFiberApplyGesture.js | 92 ++++++++++++++++--- .../src/ReactFiberViewTransitionComponent.js | 3 +- 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiber.js b/packages/react-reconciler/src/ReactFiber.js index 70cb3ed9de023..f6011a3ba155c 100644 --- a/packages/react-reconciler/src/ReactFiber.js +++ b/packages/react-reconciler/src/ReactFiber.js @@ -878,6 +878,7 @@ export function createFiberFromViewTransition( const instance: ViewTransitionState = { autoName: null, paired: null, + clones: null, ref: null, }; fiber.stateNode = instance; diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 6847a2426c5ac..2c9ffc5605acb 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -13,6 +13,8 @@ import type {Instance, TextInstance} from './ReactFiberConfig'; import type {OffscreenState} from './ReactFiberActivityComponent'; +import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; + import { cloneMutableInstance, cloneMutableTextInstance, @@ -62,6 +64,7 @@ let unhideHostChildren = false; function recursivelyInsertClonesFromExistingTree( parentFiber: Fiber, hostParentClone: Instance, + parentViewTransition: null | ViewTransitionState, ): void { let child = parentFiber.child; while (child !== null) { @@ -71,6 +74,13 @@ function recursivelyInsertClonesFromExistingTree( // If we have no mutations in this subtree, we just need to make a deep clone. const clone: Instance = cloneMutableInstance(instance, true); appendChild(hostParentClone, clone); + if (parentViewTransition !== null) { + if (parentViewTransition.clones === null) { + parentViewTransition.clones = [clone]; + } else { + parentViewTransition.clones.push(clone); + } + } // TODO: We may need to transfer some DOM state such as scroll position // for the deep clones. // TODO: If there's a manual view-transition-name inside the clone we @@ -109,20 +119,34 @@ function recursivelyInsertClonesFromExistingTree( // TODO: If this is visible but detached it should still be cloned. // Since there was no mutation to this node, it couldn't have changed // visibility so we don't need to update unhideHostChildren here. - recursivelyInsertClonesFromExistingTree(child, hostParentClone); + recursivelyInsertClonesFromExistingTree( + child, + hostParentClone, + parentViewTransition, + ); } break; } case ViewTransitionComponent: const prevMutationContext = pushMutationContext(); + const viewTransitionState: ViewTransitionState = child.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. - recursivelyInsertClonesFromExistingTree(child, hostParentClone); + viewTransitionState.clones = null; + recursivelyInsertClonesFromExistingTree( + child, + hostParentClone, + viewTransitionState, + ); // TODO: Do we need to track whether this should have a name applied? // child.flags |= Update; popMutationContext(prevMutationContext); break; default: { - recursivelyInsertClonesFromExistingTree(child, hostParentClone); + recursivelyInsertClonesFromExistingTree( + child, + hostParentClone, + parentViewTransition, + ); break; } } @@ -133,6 +157,7 @@ function recursivelyInsertClonesFromExistingTree( function recursivelyInsertClones( parentFiber: Fiber, hostParentClone: Instance, + parentViewTransition: null | ViewTransitionState, ) { const deletions = parentFiber.deletions; if (deletions !== null) { @@ -149,19 +174,28 @@ function recursivelyInsertClones( // If we have mutations or if this is a newly inserted tree, clone as we go. let child = parentFiber.child; while (child !== null) { - insertDestinationClonesOfFiber(child, hostParentClone); + insertDestinationClonesOfFiber( + child, + hostParentClone, + parentViewTransition, + ); child = child.sibling; } } else { // Once we reach a subtree with no more mutations we can bail out. // However, we must still insert deep clones of the HostComponents. - recursivelyInsertClonesFromExistingTree(parentFiber, hostParentClone); + recursivelyInsertClonesFromExistingTree( + parentFiber, + hostParentClone, + parentViewTransition, + ); } } function insertDestinationClonesOfFiber( finishedWork: Fiber, hostParentClone: Instance, + parentViewTransition: null | ViewTransitionState, ) { const current = finishedWork.alternate; const flags = finishedWork.flags; @@ -172,14 +206,22 @@ function insertDestinationClonesOfFiber( case HostHoistable: { if (supportsResources) { // TODO: Hoistables should get optimistically inserted and then removed. - recursivelyInsertClones(finishedWork, hostParentClone); + recursivelyInsertClones( + finishedWork, + hostParentClone, + parentViewTransition, + ); break; } // Fall through } case HostSingleton: { if (supportsSingletons) { - recursivelyInsertClones(finishedWork, hostParentClone); + recursivelyInsertClones( + finishedWork, + hostParentClone, + parentViewTransition, + ); if (__DEV__) { // We cannot apply mutations to Host Singletons since by definition // they cannot be cloned. Therefore we warn in DEV if this commit @@ -230,12 +272,13 @@ function insertDestinationClonesOfFiber( } case HostComponent: { const instance: Instance = finishedWork.stateNode; + let clone: Instance; if (current === null) { // For insertions we don't need to clone. It's already new state node. // TODO: Do we need to visit it for ViewTransitions though? appendChild(hostParentClone, instance); + clone = instance; } else { - let clone: Instance; if (finishedWork.child === null) { // This node is terminal. We still do a deep clone in case this has user // inserted content, text content or dangerouslySetInnerHTML. @@ -259,15 +302,22 @@ function insertDestinationClonesOfFiber( if (unhideHostChildren) { unhideHostChildren = false; - recursivelyInsertClones(finishedWork, clone); + recursivelyInsertClones(finishedWork, clone, null); appendChild(hostParentClone, clone); unhideHostChildren = true; unhideInstance(clone, finishedWork.memoizedProps); } else { - recursivelyInsertClones(finishedWork, clone); + recursivelyInsertClones(finishedWork, clone, null); appendChild(hostParentClone, clone); } } + if (parentViewTransition !== null) { + if (parentViewTransition.clones === null) { + parentViewTransition.clones = [clone]; + } else { + parentViewTransition.clones.push(clone); + } + } break; } case HostText: { @@ -308,15 +358,25 @@ function insertDestinationClonesOfFiber( // TODO: If this is visible but detached it should still be cloned. const prevUnhide = unhideHostChildren; unhideHostChildren = prevUnhide || (flags & Visibility) !== NoFlags; - recursivelyInsertClones(finishedWork, hostParentClone); + recursivelyInsertClones( + finishedWork, + hostParentClone, + parentViewTransition, + ); unhideHostChildren = prevUnhide; } break; } case ViewTransitionComponent: const prevMutationContext = pushMutationContext(); + const viewTransitionState: ViewTransitionState = finishedWork.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. - recursivelyInsertClones(finishedWork, hostParentClone); + viewTransitionState.clones = null; + recursivelyInsertClones( + finishedWork, + hostParentClone, + viewTransitionState, + ); if (viewTransitionMutationContext) { // Track that this boundary had a mutation and therefore needs to animate // whether it resized or not. @@ -325,7 +385,11 @@ function insertDestinationClonesOfFiber( popMutationContext(prevMutationContext); break; default: { - recursivelyInsertClones(finishedWork, hostParentClone); + recursivelyInsertClones( + finishedWork, + hostParentClone, + parentViewTransition, + ); break; } } @@ -356,7 +420,7 @@ export function insertDestinationClones( // Clone the whole root const rootClone = cloneRootViewTransitionContainer(root.containerInfo); root.gestureClone = rootClone; - recursivelyInsertClones(finishedWork, rootClone); + recursivelyInsertClones(finishedWork, rootClone, null); } else { root.gestureClone = null; cancelRootViewTransitionName(root.containerInfo); diff --git a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js index 16b5ef304974e..ecacf2a439944 100644 --- a/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js +++ b/packages/react-reconciler/src/ReactFiberViewTransitionComponent.js @@ -9,7 +9,7 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {FiberRoot} from './ReactInternalTypes'; -import type {ViewTransitionInstance} from './ReactFiberConfig'; +import type {ViewTransitionInstance, Instance} from './ReactFiberConfig'; import { getWorkInProgressRoot, @@ -45,6 +45,7 @@ export type ViewTransitionProps = { export type ViewTransitionState = { autoName: null | string, // the view-transition-name to use when an explicit one is not specified paired: null | ViewTransitionState, // a temporary state during the commit phase if we have paired this with another instance + clones: null | Array, // a temporary state during the apply gesture phase if we cloned this boundary ref: null | ViewTransitionInstance, // the current ref instance. This can change through the lifetime of the instance. }; From bde0ca5c13db2f470dfe08c72f5fd2d161e9e0c4 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 10 Mar 2025 17:55:09 -0400 Subject: [PATCH 04/14] Visit deletions in the clone phase We do this so that we can collect any exiting pairs that will have corresponding insertions. I don't love that we do that here instead of a previous phase but it would require another pass. --- .../src/ReactFiberApplyGesture.js | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 2c9ffc5605acb..973cb047cb4b1 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -13,7 +13,10 @@ import type {Instance, TextInstance} from './ReactFiberConfig'; import type {OffscreenState} from './ReactFiberActivityComponent'; -import type {ViewTransitionState} from './ReactFiberViewTransitionComponent'; +import type { + ViewTransitionState, + ViewTransitionProps, +} from './ReactFiberViewTransitionComponent'; import { cloneMutableInstance, @@ -42,6 +45,8 @@ import { ContentReset, NoFlags, Visibility, + ViewTransitionNamedStatic, + ViewTransitionStatic, } from './ReactFiberFlags'; import { HostComponent, @@ -61,6 +66,52 @@ function detectMutationOrInsertClones(finishedWork: Fiber): boolean { let unhideHostChildren = false; +function trackDeletedPairViewTransitions(deletion: Fiber): void { + if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) { + // This has no named view transitions in its subtree. + return; + } + let child = deletion.child; + while (child !== null) { + if (child.tag === OffscreenComponent && child.memoizedState === null) { + // This tree was already hidden so we skip it. + } else { + if ( + child.tag === ViewTransitionComponent && + (child.flags & ViewTransitionNamedStatic) !== NoFlags + ) { + const props: ViewTransitionProps = child.memoizedProps; + const name = props.name; + if (name != null && name !== 'auto') { + // TODO: Find a pair + } + } + trackDeletedPairViewTransitions(child); + } + child = child.sibling; + } +} + +function trackExitViewTransitions(deletion: Fiber): void { + if (deletion.tag === ViewTransitionComponent) { + const props: ViewTransitionProps = deletion.memoizedProps; + const name = props.name; + if (name != null && name !== 'auto') { + // TODO: Find a pair + } + // Look for more pairs deeper in the tree. + trackDeletedPairViewTransitions(deletion); + } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) { + let child = deletion.child; + while (child !== null) { + trackExitViewTransitions(child); + child = child.sibling; + } + } else { + trackDeletedPairViewTransitions(deletion); + } +} + function recursivelyInsertClonesFromExistingTree( parentFiber: Fiber, hostParentClone: Instance, @@ -162,8 +213,8 @@ function recursivelyInsertClones( const deletions = parentFiber.deletions; if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { - // const childToDelete = deletions[i]; - // TODO + const childToDelete = deletions[i]; + trackExitViewTransitions(childToDelete); } } @@ -364,6 +415,9 @@ function insertDestinationClonesOfFiber( parentViewTransition, ); unhideHostChildren = prevUnhide; + } else if (current !== null && current.memoizedState === null) { + // Was previously mounted as visible but is now hidden. + trackExitViewTransitions(current); } break; } From 1504528932d6215887a1f604435baddcda19a35a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 10 Mar 2025 17:59:35 -0400 Subject: [PATCH 05/14] Track the various states we can be in while cloning a tree --- .../src/ReactFiberApplyGesture.js | 94 +++++++++++++++---- 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 973cb047cb4b1..630b059f7417e 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -64,7 +64,12 @@ function detectMutationOrInsertClones(finishedWork: Fiber): boolean { return true; } -let unhideHostChildren = false; +const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout. +const CLONE_ENTER = 1; // Inside a reappearing offscreen but before the next host component. +const CLONE_UNHIDE = 2; // Like ENTER but we're already inside the entering View Transition. +const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs. +const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it. +let cloneState: 0 | 1 | 2 | 3 | 4 = 0; function trackDeletedPairViewTransitions(deletion: Fiber): void { if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) { @@ -122,8 +127,38 @@ function recursivelyInsertClonesFromExistingTree( switch (child.tag) { case HostComponent: { const instance: Instance = child.stateNode; - // If we have no mutations in this subtree, we just need to make a deep clone. - const clone: Instance = cloneMutableInstance(instance, true); + let keepTraversing: boolean; + switch (cloneState) { + case CLONE_UPDATE: + case CLONE_UNCHANGED: + // We've found any "layout" View Transitions at this point so we can bail. + keepTraversing = false; + break; + case CLONE_ENTER: + case CLONE_UNHIDE: + case CLONE_APPEARING_PAIR: + // If this was an unhide, we need to keep going if there are any named + // pairs in this subtree, since they might need to be marked. + keepTraversing = + (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags; + break; + } + let clone: Instance; + if (keepTraversing) { + // We might need a handle on these clones, so we need to do a shallow clone + // and keep going. + clone = cloneMutableInstance(instance, false); + recursivelyInsertClonesFromExistingTree(child, clone, null); + } else { + // If we have no mutations in this subtree, and we don't need a handle on the + // clones, then we can do a deep clone instead and bailout. + clone = cloneMutableInstance(instance, true); + // TODO: We may need to transfer some DOM state such as scroll position + // for the deep clones. + // TODO: If there's a manual view-transition-name inside the clone we + // should ideally remove it from the original and then restore it in mutation + // phase. Otherwise it leads to duplicate names. + } appendChild(hostParentClone, clone); if (parentViewTransition !== null) { if (parentViewTransition.clones === null) { @@ -132,12 +167,7 @@ function recursivelyInsertClonesFromExistingTree( parentViewTransition.clones.push(clone); } } - // TODO: We may need to transfer some DOM state such as scroll position - // for the deep clones. - // TODO: If there's a manual view-transition-name inside the clone we - // should ideally remove it from the original and then restore it in mutation - // phase. Otherwise it leads to duplicate names. - if (unhideHostChildren) { + if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { unhideInstance(clone, child.memoizedProps); } break; @@ -152,7 +182,7 @@ function recursivelyInsertClonesFromExistingTree( } const clone = cloneMutableTextInstance(textInstance); appendChild(hostParentClone, clone); - if (unhideHostChildren) { + if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { unhideTextInstance(clone, child.memoizedProps); } break; @@ -169,7 +199,7 @@ function recursivelyInsertClonesFromExistingTree( // clone invisible content. // TODO: If this is visible but detached it should still be cloned. // Since there was no mutation to this node, it couldn't have changed - // visibility so we don't need to update unhideHostChildren here. + // visibility so we don't need to update cloneState here. recursivelyInsertClonesFromExistingTree( child, hostParentClone, @@ -188,7 +218,18 @@ function recursivelyInsertClonesFromExistingTree( hostParentClone, viewTransitionState, ); - // TODO: Do we need to track whether this should have a name applied? + if (cloneState === CLONE_ENTER) { + // This was an Enter of a ViewTransition. We now move onto unhiding the inner + // HostComponents and finding inner pairs. + cloneState = CLONE_UNHIDE; + // TODO: Mark the name and find a pair. + } else if (cloneState === CLONE_UPDATE) { + // If the tree had no mutations and we've found the top most ViewTransition + // then this is the one we might apply the "layout" state too if it has changed + // position. After we've found its HostComponents we can bail out. + cloneState = CLONE_UNCHANGED; + } + // TODO: Only the first level should track if this was s // child.flags |= Update; popMutationContext(prevMutationContext); break; @@ -351,11 +392,12 @@ function insertDestinationClonesOfFiber( commitUpdate(clone, type, oldProps, newProps, finishedWork); } - if (unhideHostChildren) { - unhideHostChildren = false; + if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { + const prevCloneState = cloneState; + cloneState = CLONE_APPEARING_PAIR; recursivelyInsertClones(finishedWork, clone, null); appendChild(hostParentClone, clone); - unhideHostChildren = true; + cloneState = prevCloneState; unhideInstance(clone, finishedWork.memoizedProps); } else { recursivelyInsertClones(finishedWork, clone, null); @@ -390,7 +432,7 @@ function insertDestinationClonesOfFiber( commitTextUpdate(clone, newText, oldText); } appendChild(hostParentClone, clone); - if (unhideHostChildren) { + if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { unhideTextInstance(clone, finishedWork.memoizedProps); } } @@ -407,14 +449,20 @@ function insertDestinationClonesOfFiber( // Only insert clones if this tree is going to be visible. No need to // clone invisible content. // TODO: If this is visible but detached it should still be cloned. - const prevUnhide = unhideHostChildren; - unhideHostChildren = prevUnhide || (flags & Visibility) !== NoFlags; + const prevCloneState = cloneState; + if ( + prevCloneState === CLONE_UPDATE && + (flags & Visibility) !== NoFlags + ) { + // This is the root of an appear. We need to trigger Enter transitions. + cloneState = CLONE_ENTER; + } recursivelyInsertClones( finishedWork, hostParentClone, parentViewTransition, ); - unhideHostChildren = prevUnhide; + cloneState = prevCloneState; } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. trackExitViewTransitions(current); @@ -436,6 +484,12 @@ function insertDestinationClonesOfFiber( // whether it resized or not. finishedWork.flags |= Update; } + if (cloneState === CLONE_ENTER) { + // This was an Enter of a ViewTransition. We now move onto unhiding the inner + // HostComponents and finding inner pairs. + cloneState = CLONE_UNHIDE; + // TODO: Mark the name and find a pair. + } popMutationContext(prevMutationContext); break; default: { @@ -455,7 +509,7 @@ export function insertDestinationClones( root: FiberRoot, finishedWork: Fiber, ): void { - unhideHostChildren = false; + cloneState = CLONE_UPDATE; // We'll either not transition the root, or we'll transition the clone. Regardless // we cancel the root view transition name. const needsClone = detectMutationOrInsertClones(finishedWork); From c2e6f9ab97ce4dbf1ae26aa3e696e5aa3f72ed1b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 11 Mar 2025 01:08:57 -0400 Subject: [PATCH 06/14] Track insertions separately --- .../src/ReactFiberApplyGesture.js | 333 +++++++++++++----- 1 file changed, 236 insertions(+), 97 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 630b059f7417e..c391399e4b61d 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -65,11 +65,14 @@ function detectMutationOrInsertClones(finishedWork: Fiber): boolean { } const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout. -const CLONE_ENTER = 1; // Inside a reappearing offscreen but before the next host component. -const CLONE_UNHIDE = 2; // Like ENTER but we're already inside the entering View Transition. +const CLONE_ENTER = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent. +const CLONE_UNHIDE = 2; // Inside a reappearing offscreen before the next HostComponent. const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs. const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it. -let cloneState: 0 | 1 | 2 | 3 | 4 = 0; +const INSERT_ENTER = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent. +const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent. +const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs. +let cloneState: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 = 0; function trackDeletedPairViewTransitions(deletion: Fiber): void { if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) { @@ -117,6 +120,146 @@ function trackExitViewTransitions(deletion: Fiber): void { } } +function recursivelyInsertNew( + parentFiber: Fiber, + hostParentClone: Instance, + parentViewTransition: null | ViewTransitionState, +): void { + if ( + cloneState === INSERT_APPEARING_PAIR && + (parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags + ) { + // We're just searching for pairs but we have reached the end. + return; + } + let child = parentFiber.child; + while (child !== null) { + recursivelyInsertNewFiber(child, hostParentClone, parentViewTransition); + child = child.sibling; + } +} + +function recursivelyInsertNewFiber( + finishedWork: Fiber, + hostParentClone: Instance, + parentViewTransition: null | ViewTransitionState, +): void { + switch (finishedWork.tag) { + case HostHoistable: { + if (supportsResources) { + // TODO: Hoistables should get optimistically inserted and then removed. + recursivelyInsertNew( + finishedWork, + hostParentClone, + parentViewTransition, + ); + break; + } + // Fall through + } + case HostSingleton: { + if (supportsSingletons) { + recursivelyInsertNew( + finishedWork, + hostParentClone, + parentViewTransition, + ); + if (__DEV__) { + // We cannot apply mutations to Host Singletons since by definition + // they cannot be cloned. Therefore we warn in DEV if this commit + // had any effect. + if (finishedWork.flags & Update) { + console.error( + 'useSwipeTransition() caused something to render a new <%s>. ' + + 'This is not possible in the current implementation. ' + + "Make sure that the swipe doesn't mount any new <%s> elements.", + finishedWork.type, + finishedWork.type, + ); + } + } + break; + } + // Fall through + } + case HostComponent: { + const instance: Instance = finishedWork.stateNode; + // For insertions we don't need to clone. It's already new state node. + if (cloneState !== INSERT_APPEARING_PAIR) { + appendChild(hostParentClone, instance); + if (parentViewTransition !== null) { + if (parentViewTransition.clones === null) { + parentViewTransition.clones = [instance]; + } else { + parentViewTransition.clones.push(instance); + } + } + const prevCloneState = cloneState; + cloneState = INSERT_APPEARING_PAIR; + recursivelyInsertNew(finishedWork, instance, null); + cloneState = prevCloneState; + } else { + recursivelyInsertNew(finishedWork, instance, null); + } + break; + } + case HostText: { + const textInstance: TextInstance = finishedWork.stateNode; + if (textInstance === null) { + throw new Error( + 'This should have a text node initialized. This error is likely ' + + 'caused by a bug in React. Please file an issue.', + ); + } + // For insertions we don't need to clone. It's already new state node. + if (cloneState !== INSERT_APPEARING_PAIR) { + appendChild(hostParentClone, textInstance); + } + break; + } + case HostPortal: { + // TODO: Consider what should happen to Portals. For now we exclude them. + break; + } + case OffscreenComponent: { + const newState: OffscreenState | null = finishedWork.memoizedState; + const isHidden = newState !== null; + if (!isHidden) { + // Only insert nodes if this tree is going to be visible. No need to + // insert invisible content. + // Since there was no mutation to this node, it couldn't have changed + // visibility so we don't need to update cloneState here. + recursivelyInsertNew( + finishedWork, + hostParentClone, + parentViewTransition, + ); + } + break; + } + case ViewTransitionComponent: + const prevMutationContext = pushMutationContext(); + const viewTransitionState: ViewTransitionState = finishedWork.stateNode; + // TODO: If this was already cloned by a previous pass we can reuse those clones. + viewTransitionState.clones = null; + const prevCloneState = cloneState; + if (cloneState === INSERT_ENTER) { + // This was an Enter of a ViewTransition. We now move onto inserting the inner + // HostComponents and finding inner pairs. + cloneState = INSERT_APPEND; + // TODO: Mark the name and find a pair. + } + recursivelyInsertNew(finishedWork, hostParentClone, viewTransitionState); + cloneState = prevCloneState; + popMutationContext(prevMutationContext); + break; + default: { + recursivelyInsertNew(finishedWork, hostParentClone, parentViewTransition); + break; + } + } +} + function recursivelyInsertClonesFromExistingTree( parentFiber: Fiber, hostParentClone: Instance, @@ -213,11 +356,7 @@ function recursivelyInsertClonesFromExistingTree( const viewTransitionState: ViewTransitionState = child.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; - recursivelyInsertClonesFromExistingTree( - child, - hostParentClone, - viewTransitionState, - ); + const prevCloneState = cloneState; if (cloneState === CLONE_ENTER) { // This was an Enter of a ViewTransition. We now move onto unhiding the inner // HostComponents and finding inner pairs. @@ -229,6 +368,12 @@ function recursivelyInsertClonesFromExistingTree( // position. After we've found its HostComponents we can bail out. cloneState = CLONE_UNCHANGED; } + recursivelyInsertClonesFromExistingTree( + child, + hostParentClone, + viewTransitionState, + ); + cloneState = prevCloneState; // TODO: Only the first level should track if this was s // child.flags |= Update; popMutationContext(prevMutationContext); @@ -290,6 +435,20 @@ function insertDestinationClonesOfFiber( parentViewTransition: null | ViewTransitionState, ) { const current = finishedWork.alternate; + if (current === null) { + // This is a newly mounted subtree. Insert any HostComponents and trigger + // Enter transitions. + const prevCloneState = cloneState; + cloneState = INSERT_ENTER; + recursivelyInsertNewFiber( + finishedWork, + hostParentClone, + parentViewTransition, + ); + cloneState = prevCloneState; + return; + } + const flags = finishedWork.flags; // The effect flag should be checked *after* we refine the type of fiber, // because the fiber tag is more specific. An exception is any flag related @@ -319,42 +478,32 @@ function insertDestinationClonesOfFiber( // they cannot be cloned. Therefore we warn in DEV if this commit // had any effect. if (flags & Update) { - if (current === null) { - console.error( - 'useSwipeTransition() caused something to render a new <%s>. ' + - 'This is not possible in the current implementation. ' + - "Make sure that the swipe doesn't mount any new <%s> elements.", - finishedWork.type, - finishedWork.type, - ); - } else { - const newProps = finishedWork.memoizedProps; - const oldProps = current.memoizedProps; - const instance = finishedWork.stateNode; - const type = finishedWork.type; - const prev = pushMutationContext(); + const newProps = finishedWork.memoizedProps; + const oldProps = current.memoizedProps; + const instance = finishedWork.stateNode; + const type = finishedWork.type; + const prev = pushMutationContext(); - try { - // Since we currently don't have a separate diffing algorithm for - // individual properties, the Update flag can be a false positive. - // We have to apply the new props first o detect any mutations and - // then revert them. - commitUpdate(instance, type, oldProps, newProps, finishedWork); - if (viewTransitionMutationContext) { - console.error( - 'useSwipeTransition() caused something to mutate <%s>. ' + - 'This is not possible in the current implementation. ' + - "Make sure that the swipe doesn't update any state which " + - 'causes <%s> to change.', - finishedWork.type, - finishedWork.type, - ); - } - // Revert - commitUpdate(instance, type, newProps, oldProps, finishedWork); - } finally { - popMutationContext(prev); + try { + // Since we currently don't have a separate diffing algorithm for + // individual properties, the Update flag can be a false positive. + // We have to apply the new props first o detect any mutations and + // then revert them. + commitUpdate(instance, type, oldProps, newProps, finishedWork); + if (viewTransitionMutationContext) { + console.error( + 'useSwipeTransition() caused something to mutate <%s>. ' + + 'This is not possible in the current implementation. ' + + "Make sure that the swipe doesn't update any state which " + + 'causes <%s> to change.', + finishedWork.type, + finishedWork.type, + ); } + // Revert + commitUpdate(instance, type, newProps, oldProps, finishedWork); + } finally { + popMutationContext(prev); } } } @@ -365,44 +514,37 @@ function insertDestinationClonesOfFiber( case HostComponent: { const instance: Instance = finishedWork.stateNode; let clone: Instance; - if (current === null) { - // For insertions we don't need to clone. It's already new state node. - // TODO: Do we need to visit it for ViewTransitions though? - appendChild(hostParentClone, instance); - clone = instance; - } else { - if (finishedWork.child === null) { - // This node is terminal. We still do a deep clone in case this has user - // inserted content, text content or dangerouslySetInnerHTML. - clone = cloneMutableInstance(instance, true); - if (finishedWork.flags & ContentReset) { - resetTextContent(clone); - } - } else { - // If we have children we'll clone them as we walk the tree so we just - // do a shallow clone here. - clone = cloneMutableInstance(instance, false); + if (finishedWork.child === null) { + // This node is terminal. We still do a deep clone in case this has user + // inserted content, text content or dangerouslySetInnerHTML. + clone = cloneMutableInstance(instance, true); + if (finishedWork.flags & ContentReset) { + resetTextContent(clone); } + } else { + // If we have children we'll clone them as we walk the tree so we just + // do a shallow clone here. + clone = cloneMutableInstance(instance, false); + } - if (flags & Update) { - const newProps = finishedWork.memoizedProps; - const oldProps = current.memoizedProps; - const type = finishedWork.type; - // Apply the delta to the clone. - commitUpdate(clone, type, oldProps, newProps, finishedWork); - } + if (flags & Update) { + const newProps = finishedWork.memoizedProps; + const oldProps = current.memoizedProps; + const type = finishedWork.type; + // Apply the delta to the clone. + commitUpdate(clone, type, oldProps, newProps, finishedWork); + } - if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { - const prevCloneState = cloneState; - cloneState = CLONE_APPEARING_PAIR; - recursivelyInsertClones(finishedWork, clone, null); - appendChild(hostParentClone, clone); - cloneState = prevCloneState; - unhideInstance(clone, finishedWork.memoizedProps); - } else { - recursivelyInsertClones(finishedWork, clone, null); - appendChild(hostParentClone, clone); - } + if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { + const prevCloneState = cloneState; + cloneState = CLONE_APPEARING_PAIR; + recursivelyInsertClones(finishedWork, clone, null); + appendChild(hostParentClone, clone); + cloneState = prevCloneState; + unhideInstance(clone, finishedWork.memoizedProps); + } else { + recursivelyInsertClones(finishedWork, clone, null); + appendChild(hostParentClone, clone); } if (parentViewTransition !== null) { if (parentViewTransition.clones === null) { @@ -421,20 +563,15 @@ function insertDestinationClonesOfFiber( 'caused by a bug in React. Please file an issue.', ); } - if (current === null) { - // For insertions we don't need to clone. It's already new state node. - appendChild(hostParentClone, textInstance); - } else { - const clone = cloneMutableTextInstance(textInstance); - if (flags & Update) { - const newText: string = finishedWork.memoizedProps; - const oldText: string = current.memoizedProps; - commitTextUpdate(clone, newText, oldText); - } - appendChild(hostParentClone, clone); - if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { - unhideTextInstance(clone, finishedWork.memoizedProps); - } + const clone = cloneMutableTextInstance(textInstance); + if (flags & Update) { + const newText: string = finishedWork.memoizedProps; + const oldText: string = current.memoizedProps; + commitTextUpdate(clone, newText, oldText); + } + appendChild(hostParentClone, clone); + if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { + unhideTextInstance(clone, finishedWork.memoizedProps); } break; } @@ -474,6 +611,13 @@ function insertDestinationClonesOfFiber( const viewTransitionState: ViewTransitionState = finishedWork.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; + const prevCloneState = cloneState; + if (cloneState === CLONE_ENTER) { + // This was an Enter of a ViewTransition. We now move onto unhiding the inner + // HostComponents and finding inner pairs. + cloneState = CLONE_UNHIDE; + // TODO: Mark the name and find a pair. + } recursivelyInsertClones( finishedWork, hostParentClone, @@ -484,12 +628,7 @@ function insertDestinationClonesOfFiber( // whether it resized or not. finishedWork.flags |= Update; } - if (cloneState === CLONE_ENTER) { - // This was an Enter of a ViewTransition. We now move onto unhiding the inner - // HostComponents and finding inner pairs. - cloneState = CLONE_UNHIDE; - // TODO: Mark the name and find a pair. - } + cloneState = prevCloneState; popMutationContext(prevMutationContext); break; default: { From 2bad585bb8bc65820212c2d1c25d6845cf73044d Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 11 Mar 2025 12:48:47 -0400 Subject: [PATCH 07/14] Move cloneState into an argument This makes it a bit easier not to mess it up at the cost of some more stack allocation (which may cause overflow). --- .../src/ReactFiberApplyGesture.js | 143 +++++++++++------- 1 file changed, 92 insertions(+), 51 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index c391399e4b61d..0efe0f08f0272 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -72,7 +72,7 @@ const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still w const INSERT_ENTER = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent. const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent. const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs. -let cloneState: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 = 0; +type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; function trackDeletedPairViewTransitions(deletion: Fiber): void { if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) { @@ -124,9 +124,10 @@ function recursivelyInsertNew( parentFiber: Fiber, hostParentClone: Instance, parentViewTransition: null | ViewTransitionState, + visitPhase: VisitPhase, ): void { if ( - cloneState === INSERT_APPEARING_PAIR && + visitPhase === INSERT_APPEARING_PAIR && (parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags ) { // We're just searching for pairs but we have reached the end. @@ -134,7 +135,12 @@ function recursivelyInsertNew( } let child = parentFiber.child; while (child !== null) { - recursivelyInsertNewFiber(child, hostParentClone, parentViewTransition); + recursivelyInsertNewFiber( + child, + hostParentClone, + parentViewTransition, + visitPhase, + ); child = child.sibling; } } @@ -143,6 +149,7 @@ function recursivelyInsertNewFiber( finishedWork: Fiber, hostParentClone: Instance, parentViewTransition: null | ViewTransitionState, + visitPhase: VisitPhase, ): void { switch (finishedWork.tag) { case HostHoistable: { @@ -152,6 +159,7 @@ function recursivelyInsertNewFiber( finishedWork, hostParentClone, parentViewTransition, + visitPhase, ); break; } @@ -163,7 +171,9 @@ function recursivelyInsertNewFiber( finishedWork, hostParentClone, parentViewTransition, + visitPhase, ); + if (__DEV__) { // We cannot apply mutations to Host Singletons since by definition // they cannot be cloned. Therefore we warn in DEV if this commit @@ -185,7 +195,7 @@ function recursivelyInsertNewFiber( case HostComponent: { const instance: Instance = finishedWork.stateNode; // For insertions we don't need to clone. It's already new state node. - if (cloneState !== INSERT_APPEARING_PAIR) { + if (visitPhase !== INSERT_APPEARING_PAIR) { appendChild(hostParentClone, instance); if (parentViewTransition !== null) { if (parentViewTransition.clones === null) { @@ -194,12 +204,14 @@ function recursivelyInsertNewFiber( parentViewTransition.clones.push(instance); } } - const prevCloneState = cloneState; - cloneState = INSERT_APPEARING_PAIR; - recursivelyInsertNew(finishedWork, instance, null); - cloneState = prevCloneState; + recursivelyInsertNew( + finishedWork, + instance, + null, + INSERT_APPEARING_PAIR, + ); } else { - recursivelyInsertNew(finishedWork, instance, null); + recursivelyInsertNew(finishedWork, instance, null, visitPhase); } break; } @@ -212,7 +224,7 @@ function recursivelyInsertNewFiber( ); } // For insertions we don't need to clone. It's already new state node. - if (cloneState !== INSERT_APPEARING_PAIR) { + if (visitPhase !== INSERT_APPEARING_PAIR) { appendChild(hostParentClone, textInstance); } break; @@ -228,11 +240,12 @@ function recursivelyInsertNewFiber( // Only insert nodes if this tree is going to be visible. No need to // insert invisible content. // Since there was no mutation to this node, it couldn't have changed - // visibility so we don't need to update cloneState here. + // visibility so we don't need to update visitPhase here. recursivelyInsertNew( finishedWork, hostParentClone, parentViewTransition, + visitPhase, ); } break; @@ -242,19 +255,30 @@ function recursivelyInsertNewFiber( const viewTransitionState: ViewTransitionState = finishedWork.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; - const prevCloneState = cloneState; - if (cloneState === INSERT_ENTER) { + let nextPhase; + if (visitPhase === INSERT_ENTER) { // This was an Enter of a ViewTransition. We now move onto inserting the inner // HostComponents and finding inner pairs. - cloneState = INSERT_APPEND; + nextPhase = INSERT_APPEND; // TODO: Mark the name and find a pair. + } else { + nextPhase = visitPhase; } - recursivelyInsertNew(finishedWork, hostParentClone, viewTransitionState); - cloneState = prevCloneState; + recursivelyInsertNew( + finishedWork, + hostParentClone, + viewTransitionState, + nextPhase, + ); popMutationContext(prevMutationContext); break; default: { - recursivelyInsertNew(finishedWork, hostParentClone, parentViewTransition); + recursivelyInsertNew( + finishedWork, + hostParentClone, + parentViewTransition, + visitPhase, + ); break; } } @@ -264,6 +288,7 @@ function recursivelyInsertClonesFromExistingTree( parentFiber: Fiber, hostParentClone: Instance, parentViewTransition: null | ViewTransitionState, + visitPhase: VisitPhase, ): void { let child = parentFiber.child; while (child !== null) { @@ -271,7 +296,7 @@ function recursivelyInsertClonesFromExistingTree( case HostComponent: { const instance: Instance = child.stateNode; let keepTraversing: boolean; - switch (cloneState) { + switch (visitPhase) { case CLONE_UPDATE: case CLONE_UNCHANGED: // We've found any "layout" View Transitions at this point so we can bail. @@ -291,7 +316,12 @@ function recursivelyInsertClonesFromExistingTree( // We might need a handle on these clones, so we need to do a shallow clone // and keep going. clone = cloneMutableInstance(instance, false); - recursivelyInsertClonesFromExistingTree(child, clone, null); + recursivelyInsertClonesFromExistingTree( + child, + clone, + null, + visitPhase, + ); } else { // If we have no mutations in this subtree, and we don't need a handle on the // clones, then we can do a deep clone instead and bailout. @@ -310,7 +340,7 @@ function recursivelyInsertClonesFromExistingTree( parentViewTransition.clones.push(clone); } } - if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { + if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { unhideInstance(clone, child.memoizedProps); } break; @@ -325,7 +355,7 @@ function recursivelyInsertClonesFromExistingTree( } const clone = cloneMutableTextInstance(textInstance); appendChild(hostParentClone, clone); - if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { + if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { unhideTextInstance(clone, child.memoizedProps); } break; @@ -342,11 +372,12 @@ function recursivelyInsertClonesFromExistingTree( // clone invisible content. // TODO: If this is visible but detached it should still be cloned. // Since there was no mutation to this node, it couldn't have changed - // visibility so we don't need to update cloneState here. + // visibility so we don't need to update visitPhase here. recursivelyInsertClonesFromExistingTree( child, hostParentClone, parentViewTransition, + visitPhase, ); } break; @@ -356,24 +387,26 @@ function recursivelyInsertClonesFromExistingTree( const viewTransitionState: ViewTransitionState = child.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; - const prevCloneState = cloneState; - if (cloneState === CLONE_ENTER) { + let nextPhase; + if (visitPhase === CLONE_ENTER) { // This was an Enter of a ViewTransition. We now move onto unhiding the inner // HostComponents and finding inner pairs. - cloneState = CLONE_UNHIDE; + nextPhase = CLONE_UNHIDE; // TODO: Mark the name and find a pair. - } else if (cloneState === CLONE_UPDATE) { + } else if (visitPhase === CLONE_UPDATE) { // If the tree had no mutations and we've found the top most ViewTransition // then this is the one we might apply the "layout" state too if it has changed // position. After we've found its HostComponents we can bail out. - cloneState = CLONE_UNCHANGED; + nextPhase = CLONE_UNCHANGED; + } else { + nextPhase = visitPhase; } recursivelyInsertClonesFromExistingTree( child, hostParentClone, viewTransitionState, + nextPhase, ); - cloneState = prevCloneState; // TODO: Only the first level should track if this was s // child.flags |= Update; popMutationContext(prevMutationContext); @@ -383,6 +416,7 @@ function recursivelyInsertClonesFromExistingTree( child, hostParentClone, parentViewTransition, + visitPhase, ); break; } @@ -395,6 +429,7 @@ function recursivelyInsertClones( parentFiber: Fiber, hostParentClone: Instance, parentViewTransition: null | ViewTransitionState, + visitPhase: VisitPhase, ) { const deletions = parentFiber.deletions; if (deletions !== null) { @@ -415,6 +450,7 @@ function recursivelyInsertClones( child, hostParentClone, parentViewTransition, + visitPhase, ); child = child.sibling; } @@ -425,6 +461,7 @@ function recursivelyInsertClones( parentFiber, hostParentClone, parentViewTransition, + visitPhase, ); } } @@ -433,19 +470,18 @@ function insertDestinationClonesOfFiber( finishedWork: Fiber, hostParentClone: Instance, parentViewTransition: null | ViewTransitionState, + visitPhase: VisitPhase, ) { const current = finishedWork.alternate; if (current === null) { // This is a newly mounted subtree. Insert any HostComponents and trigger // Enter transitions. - const prevCloneState = cloneState; - cloneState = INSERT_ENTER; recursivelyInsertNewFiber( finishedWork, hostParentClone, parentViewTransition, + INSERT_ENTER, ); - cloneState = prevCloneState; return; } @@ -461,6 +497,7 @@ function insertDestinationClonesOfFiber( finishedWork, hostParentClone, parentViewTransition, + visitPhase, ); break; } @@ -472,6 +509,7 @@ function insertDestinationClonesOfFiber( finishedWork, hostParentClone, parentViewTransition, + visitPhase, ); if (__DEV__) { // We cannot apply mutations to Host Singletons since by definition @@ -535,15 +573,17 @@ function insertDestinationClonesOfFiber( commitUpdate(clone, type, oldProps, newProps, finishedWork); } - if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { - const prevCloneState = cloneState; - cloneState = CLONE_APPEARING_PAIR; - recursivelyInsertClones(finishedWork, clone, null); + if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { + recursivelyInsertClones( + finishedWork, + clone, + null, + CLONE_APPEARING_PAIR, + ); appendChild(hostParentClone, clone); - cloneState = prevCloneState; unhideInstance(clone, finishedWork.memoizedProps); } else { - recursivelyInsertClones(finishedWork, clone, null); + recursivelyInsertClones(finishedWork, clone, null, visitPhase); appendChild(hostParentClone, clone); } if (parentViewTransition !== null) { @@ -570,7 +610,7 @@ function insertDestinationClonesOfFiber( commitTextUpdate(clone, newText, oldText); } appendChild(hostParentClone, clone); - if (cloneState === CLONE_ENTER || cloneState === CLONE_UNHIDE) { + if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { unhideTextInstance(clone, finishedWork.memoizedProps); } break; @@ -586,20 +626,19 @@ function insertDestinationClonesOfFiber( // Only insert clones if this tree is going to be visible. No need to // clone invisible content. // TODO: If this is visible but detached it should still be cloned. - const prevCloneState = cloneState; - if ( - prevCloneState === CLONE_UPDATE && - (flags & Visibility) !== NoFlags - ) { + let nextPhase; + if (visitPhase === CLONE_UPDATE && (flags & Visibility) !== NoFlags) { // This is the root of an appear. We need to trigger Enter transitions. - cloneState = CLONE_ENTER; + nextPhase = CLONE_ENTER; + } else { + nextPhase = visitPhase; } recursivelyInsertClones( finishedWork, hostParentClone, parentViewTransition, + nextPhase, ); - cloneState = prevCloneState; } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. trackExitViewTransitions(current); @@ -611,24 +650,26 @@ function insertDestinationClonesOfFiber( const viewTransitionState: ViewTransitionState = finishedWork.stateNode; // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; - const prevCloneState = cloneState; - if (cloneState === CLONE_ENTER) { + let nextPhase; + if (visitPhase === CLONE_ENTER) { // This was an Enter of a ViewTransition. We now move onto unhiding the inner // HostComponents and finding inner pairs. - cloneState = CLONE_UNHIDE; + nextPhase = CLONE_UNHIDE; // TODO: Mark the name and find a pair. + } else { + nextPhase = visitPhase; } recursivelyInsertClones( finishedWork, hostParentClone, viewTransitionState, + nextPhase, ); if (viewTransitionMutationContext) { // Track that this boundary had a mutation and therefore needs to animate // whether it resized or not. finishedWork.flags |= Update; } - cloneState = prevCloneState; popMutationContext(prevMutationContext); break; default: { @@ -636,6 +677,7 @@ function insertDestinationClonesOfFiber( finishedWork, hostParentClone, parentViewTransition, + visitPhase, ); break; } @@ -648,7 +690,6 @@ export function insertDestinationClones( root: FiberRoot, finishedWork: Fiber, ): void { - cloneState = CLONE_UPDATE; // We'll either not transition the root, or we'll transition the clone. Regardless // we cancel the root view transition name. const needsClone = detectMutationOrInsertClones(finishedWork); @@ -667,7 +708,7 @@ export function insertDestinationClones( // Clone the whole root const rootClone = cloneRootViewTransitionContainer(root.containerInfo); root.gestureClone = rootClone; - recursivelyInsertClones(finishedWork, rootClone, null); + recursivelyInsertClones(finishedWork, rootClone, null, CLONE_UPDATE); } else { root.gestureClone = null; cancelRootViewTransitionName(root.containerInfo); From 3394bef68e9113f5827a65d6b7c4810e1fada650 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 11 Mar 2025 18:13:26 -0400 Subject: [PATCH 08/14] Visit the same View Transitions to apply names to the "new" phase This is a bit simpler because it doesn't do cloning so it matches the similar phases of ReactFiberCommitViewTransitions. The "old" phase doesn't need resetting because all the clones get deleted at the end. We don't need to visit the insertions other than just the top level "enter" because any pair will be visited by the deletion recursion. --- .../src/ReactFiberApplyGesture.js | 167 +++++++++++++++++- 1 file changed, 166 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 0efe0f08f0272..45dc002daeac7 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -715,17 +715,182 @@ export function insertDestinationClones( } } +function applyDeletedPairViewTransitions(deletion: Fiber): void { + if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) { + // This has no named view transitions in its subtree. + return; + } + let child = deletion.child; + while (child !== null) { + if (child.tag === OffscreenComponent && child.memoizedState === null) { + // This tree was already hidden so we skip it. + } else { + if ( + child.tag === ViewTransitionComponent && + (child.flags & ViewTransitionNamedStatic) !== NoFlags + ) { + const props: ViewTransitionProps = child.memoizedProps; + const name = props.name; + if (name != null && name !== 'auto') { + // TODO: Find a pair + } + } + applyDeletedPairViewTransitions(child); + } + child = child.sibling; + } +} + +function applyExitViewTransitions(deletion: Fiber): void { + if (deletion.tag === ViewTransitionComponent) { + const props: ViewTransitionProps = deletion.memoizedProps; + const name = props.name; + if (name != null && name !== 'auto') { + // TODO: Find a pair + } + // Look for more pairs deeper in the tree. + applyDeletedPairViewTransitions(deletion); + } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) { + // TODO: Check if this is a hidden Offscreen or a Portal. + let child = deletion.child; + while (child !== null) { + applyExitViewTransitions(child); + child = child.sibling; + } + } else { + applyDeletedPairViewTransitions(deletion); + } +} + +function measureEnterViewTransitions(placement: Fiber): void { + if (placement.tag === ViewTransitionComponent) { + // const state: ViewTransitionState = placement.stateNode; + const props: ViewTransitionProps = placement.memoizedProps; + const name = props.name; + if (name != null && name !== 'auto') { + // TODO: Find a pair + } + } else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) { + // TODO: Check if this is a hidden Offscreen or a Portal. + let child = placement.child; + while (child !== null) { + measureEnterViewTransitions(child); + child = child.sibling; + } + } else { + // We don't need to find pairs here because we would've already found and + // measured the pairs inside the deletion phase. + } +} + +function measureNestedViewTransitions(changedParent: Fiber): void { + let child = changedParent.child; + while (child !== null) { + if (child.tag === ViewTransitionComponent) { + const current = child.alternate; + if (current !== null) { + // const props: ViewTransitionProps = child.memoizedProps; + // const name = getViewTransitionName(props, child.stateNode); + // TODO: Measure both the old and new state and see if they're different. + } + } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) { + // TODO: Check if this is a hidden Offscreen or a Portal. + measureNestedViewTransitions(child); + } + child = child.sibling; + } +} + +function recursivelyApplyViewTransitions(parentFiber: Fiber) { + const deletions = parentFiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + applyExitViewTransitions(childToDelete); + } + } + + if ( + parentFiber.alternate === null || + (parentFiber.subtreeFlags & MutationMask) !== NoFlags + ) { + // If we have mutations or if this is a newly inserted tree, clone as we go. + let child = parentFiber.child; + while (child !== null) { + applyViewTransitionsOnFiber(child); + child = child.sibling; + } + } else { + // Nothing has changed in this subtree, but the parent may have still affected + // its size and position. We need to measure the old and new state to see if + // we should animate its size and position. + measureNestedViewTransitions(parentFiber); + } +} + +function applyViewTransitionsOnFiber(finishedWork: Fiber) { + const current = finishedWork.alternate; + if (current === null) { + measureEnterViewTransitions(finishedWork); + return; + } + + const flags = finishedWork.flags; + // The effect flag should be checked *after* we refine the type of fiber, + // because the fiber tag is more specific. An exception is any flag related + // to reconciliation, because those can be set on all fiber types. + switch (finishedWork.tag) { + case HostComponent: { + // const instance: Instance = finishedWork.stateNode; + // TODO: Apply name and measure. + recursivelyApplyViewTransitions(finishedWork); + break; + } + case HostText: { + break; + } + case HostPortal: { + // TODO: Consider what should happen to Portals. For now we exclude them. + break; + } + case OffscreenComponent: { + if (flags & Visibility) { + const newState: OffscreenState | null = finishedWork.memoizedState; + const isHidden = newState !== null; + if (!isHidden) { + measureEnterViewTransitions(finishedWork); + } else if (current !== null && current.memoizedState === null) { + // Was previously mounted as visible but is now hidden. + applyExitViewTransitions(current); + } + } + break; + } + case ViewTransitionComponent: + const viewTransitionState: ViewTransitionState = finishedWork.stateNode; + viewTransitionState.clones = null; // Reset + recursivelyApplyViewTransitions(finishedWork); + break; + default: { + recursivelyApplyViewTransitions(finishedWork); + break; + } + } +} + // Revert insertions and apply view transition names to the "new" (current) state. export function applyDepartureTransitions( root: FiberRoot, finishedWork: Fiber, ): void { + // First measure and apply view-transition-names to the "new" states. + recursivelyApplyViewTransitions(finishedWork); + // Then remove the clones. const rootClone = root.gestureClone; if (rootClone !== null) { root.gestureClone = null; removeRootViewTransitionClone(root.containerInfo, rootClone); } - // TODO } // Revert transition names and start/adjust animations on the started View Transition. From 5060d55347c6c022002ca7bad284a49545518de4 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 12 Mar 2025 00:48:04 -0400 Subject: [PATCH 09/14] Recursively restore any names added by previous phases This can reuse more from ReactFiberCommitViewTransitions because the restore code is more broad than technically necessary. --- .../src/ReactFiberApplyGesture.js | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 45dc002daeac7..f9a58bd596a01 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -57,6 +57,11 @@ import { OffscreenComponent, ViewTransitionComponent, } from './ReactWorkTags'; +import { + restoreEnterViewTransitions, + restoreExitViewTransitions, + restoreNestedViewTransitions, +} from './ReactFiberCommitViewTransitions'; let didWarnForRootClone = false; @@ -893,11 +898,88 @@ export function applyDepartureTransitions( } } +function recursivelyRestoreViewTransitions(parentFiber: Fiber) { + const deletions = parentFiber.deletions; + if (deletions !== null) { + for (let i = 0; i < deletions.length; i++) { + const childToDelete = deletions[i]; + restoreExitViewTransitions(childToDelete); + } + } + + if ( + parentFiber.alternate === null || + (parentFiber.subtreeFlags & MutationMask) !== NoFlags + ) { + // If we have mutations or if this is a newly inserted tree, clone as we go. + let child = parentFiber.child; + while (child !== null) { + restoreViewTransitionsOnFiber(child); + child = child.sibling; + } + } else { + // Nothing has changed in this subtree, but the parent may have still affected + // its size and position. We need to measure the old and new state to see if + // we should animate its size and position. + restoreNestedViewTransitions(parentFiber); + } +} + +function restoreViewTransitionsOnFiber(finishedWork: Fiber) { + const current = finishedWork.alternate; + if (current === null) { + restoreEnterViewTransitions(finishedWork); + return; + } + + const flags = finishedWork.flags; + // The effect flag should be checked *after* we refine the type of fiber, + // because the fiber tag is more specific. An exception is any flag related + // to reconciliation, because those can be set on all fiber types. + switch (finishedWork.tag) { + case HostComponent: { + // const instance: Instance = finishedWork.stateNode; + // TODO: Restore the name. + recursivelyRestoreViewTransitions(finishedWork); + break; + } + case HostText: { + break; + } + case HostPortal: { + // TODO: Consider what should happen to Portals. For now we exclude them. + break; + } + case OffscreenComponent: { + if (flags & Visibility) { + const newState: OffscreenState | null = finishedWork.memoizedState; + const isHidden = newState !== null; + if (!isHidden) { + restoreEnterViewTransitions(finishedWork); + } else if (current !== null && current.memoizedState === null) { + // Was previously mounted as visible but is now hidden. + restoreExitViewTransitions(current); + } + } + break; + } + case ViewTransitionComponent: + const viewTransitionState: ViewTransitionState = finishedWork.stateNode; + viewTransitionState.clones = null; // Reset + recursivelyRestoreViewTransitions(finishedWork); + break; + default: { + recursivelyRestoreViewTransitions(finishedWork); + break; + } + } +} + // Revert transition names and start/adjust animations on the started View Transition. export function startGestureAnimations( root: FiberRoot, finishedWork: Fiber, ): void { - // TODO + restoreViewTransitionsOnFiber(finishedWork); restoreRootViewTransitionName(root.containerInfo); } From b91d7aab8a15fffc7475c7c113a2acf8d426db3c Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 12 Mar 2025 17:37:39 -0400 Subject: [PATCH 10/14] Swap Enter and Exit terminology Since we're applying transitions in reverse, all "insertions" in the newly finished trees are actually "exits". All "deletions" are "enters". --- .../src/ReactFiberApplyGesture.js | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index f9a58bd596a01..13abf325ce549 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -58,8 +58,7 @@ import { ViewTransitionComponent, } from './ReactWorkTags'; import { - restoreEnterViewTransitions, - restoreExitViewTransitions, + restoreEnterOrExitViewTransitions, restoreNestedViewTransitions, } from './ReactFiberCommitViewTransitions'; @@ -70,11 +69,11 @@ function detectMutationOrInsertClones(finishedWork: Fiber): boolean { } const CLONE_UPDATE = 0; // Mutations in this subtree or potentially affected by layout. -const CLONE_ENTER = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent. +const CLONE_EXIT = 1; // Inside a reappearing offscreen before the next ViewTransition or HostComponent. const CLONE_UNHIDE = 2; // Inside a reappearing offscreen before the next HostComponent. const CLONE_APPEARING_PAIR = 3; // Like UNHIDE but we're already inside the first Host Component only finding pairs. const CLONE_UNCHANGED = 4; // Nothing in this tree was changed but we're still walking to clone it. -const INSERT_ENTER = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent. +const INSERT_EXIT = 5; // Inside a newly mounted tree before the next ViewTransition or HostComponent. const INSERT_APPEND = 6; // Inside a newly mounted tree before the next HostComponent. const INSERT_APPEARING_PAIR = 7; // Inside a newly mounted tree only finding pairs. type VisitPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; @@ -105,7 +104,7 @@ function trackDeletedPairViewTransitions(deletion: Fiber): void { } } -function trackExitViewTransitions(deletion: Fiber): void { +function trackEnterViewTransitions(deletion: Fiber): void { if (deletion.tag === ViewTransitionComponent) { const props: ViewTransitionProps = deletion.memoizedProps; const name = props.name; @@ -117,7 +116,7 @@ function trackExitViewTransitions(deletion: Fiber): void { } else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) { let child = deletion.child; while (child !== null) { - trackExitViewTransitions(child); + trackEnterViewTransitions(child); child = child.sibling; } } else { @@ -261,7 +260,7 @@ function recursivelyInsertNewFiber( // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; let nextPhase; - if (visitPhase === INSERT_ENTER) { + if (visitPhase === INSERT_EXIT) { // This was an Enter of a ViewTransition. We now move onto inserting the inner // HostComponents and finding inner pairs. nextPhase = INSERT_APPEND; @@ -307,7 +306,7 @@ function recursivelyInsertClonesFromExistingTree( // We've found any "layout" View Transitions at this point so we can bail. keepTraversing = false; break; - case CLONE_ENTER: + case CLONE_EXIT: case CLONE_UNHIDE: case CLONE_APPEARING_PAIR: // If this was an unhide, we need to keep going if there are any named @@ -345,7 +344,7 @@ function recursivelyInsertClonesFromExistingTree( parentViewTransition.clones.push(clone); } } - if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { unhideInstance(clone, child.memoizedProps); } break; @@ -360,7 +359,7 @@ function recursivelyInsertClonesFromExistingTree( } const clone = cloneMutableTextInstance(textInstance); appendChild(hostParentClone, clone); - if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { unhideTextInstance(clone, child.memoizedProps); } break; @@ -393,7 +392,7 @@ function recursivelyInsertClonesFromExistingTree( // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; let nextPhase; - if (visitPhase === CLONE_ENTER) { + if (visitPhase === CLONE_EXIT) { // This was an Enter of a ViewTransition. We now move onto unhiding the inner // HostComponents and finding inner pairs. nextPhase = CLONE_UNHIDE; @@ -440,7 +439,7 @@ function recursivelyInsertClones( if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; - trackExitViewTransitions(childToDelete); + trackEnterViewTransitions(childToDelete); } } @@ -485,7 +484,7 @@ function insertDestinationClonesOfFiber( finishedWork, hostParentClone, parentViewTransition, - INSERT_ENTER, + INSERT_EXIT, ); return; } @@ -578,7 +577,7 @@ function insertDestinationClonesOfFiber( commitUpdate(clone, type, oldProps, newProps, finishedWork); } - if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { recursivelyInsertClones( finishedWork, clone, @@ -615,7 +614,7 @@ function insertDestinationClonesOfFiber( commitTextUpdate(clone, newText, oldText); } appendChild(hostParentClone, clone); - if (visitPhase === CLONE_ENTER || visitPhase === CLONE_UNHIDE) { + if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { unhideTextInstance(clone, finishedWork.memoizedProps); } break; @@ -634,7 +633,7 @@ function insertDestinationClonesOfFiber( let nextPhase; if (visitPhase === CLONE_UPDATE && (flags & Visibility) !== NoFlags) { // This is the root of an appear. We need to trigger Enter transitions. - nextPhase = CLONE_ENTER; + nextPhase = CLONE_EXIT; } else { nextPhase = visitPhase; } @@ -646,7 +645,7 @@ function insertDestinationClonesOfFiber( ); } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. - trackExitViewTransitions(current); + trackEnterViewTransitions(current); } break; } @@ -656,7 +655,7 @@ function insertDestinationClonesOfFiber( // TODO: If this was already cloned by a previous pass we can reuse those clones. viewTransitionState.clones = null; let nextPhase; - if (visitPhase === CLONE_ENTER) { + if (visitPhase === CLONE_EXIT) { // This was an Enter of a ViewTransition. We now move onto unhiding the inner // HostComponents and finding inner pairs. nextPhase = CLONE_UNHIDE; @@ -746,7 +745,7 @@ function applyDeletedPairViewTransitions(deletion: Fiber): void { } } -function applyExitViewTransitions(deletion: Fiber): void { +function applyEnterViewTransitions(deletion: Fiber): void { if (deletion.tag === ViewTransitionComponent) { const props: ViewTransitionProps = deletion.memoizedProps; const name = props.name; @@ -759,7 +758,7 @@ function applyExitViewTransitions(deletion: Fiber): void { // TODO: Check if this is a hidden Offscreen or a Portal. let child = deletion.child; while (child !== null) { - applyExitViewTransitions(child); + applyEnterViewTransitions(child); child = child.sibling; } } else { @@ -811,7 +810,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; - applyExitViewTransitions(childToDelete); + applyEnterViewTransitions(childToDelete); } } @@ -866,7 +865,7 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { measureEnterViewTransitions(finishedWork); } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. - applyExitViewTransitions(current); + applyEnterViewTransitions(current); } } break; @@ -903,7 +902,7 @@ function recursivelyRestoreViewTransitions(parentFiber: Fiber) { if (deletions !== null) { for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; - restoreExitViewTransitions(childToDelete); + restoreEnterOrExitViewTransitions(childToDelete); } } @@ -928,7 +927,7 @@ function recursivelyRestoreViewTransitions(parentFiber: Fiber) { function restoreViewTransitionsOnFiber(finishedWork: Fiber) { const current = finishedWork.alternate; if (current === null) { - restoreEnterViewTransitions(finishedWork); + restoreEnterOrExitViewTransitions(finishedWork); return; } @@ -955,10 +954,10 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; if (!isHidden) { - restoreEnterViewTransitions(finishedWork); + restoreEnterOrExitViewTransitions(finishedWork); } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. - restoreExitViewTransitions(current); + restoreEnterOrExitViewTransitions(current); } } break; From 61e959682d764dc48f88e8abbe585299f629cddd Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Wed, 12 Mar 2025 23:56:08 -0400 Subject: [PATCH 11/14] Fix phase shift once we've reached a host component we're no longer in the exit/unhide phase --- .../src/ReactFiberApplyGesture.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 13abf325ce549..9411e72c1200a 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -299,24 +299,24 @@ function recursivelyInsertClonesFromExistingTree( switch (child.tag) { case HostComponent: { const instance: Instance = child.stateNode; - let keepTraversing: boolean; + let nextPhase: VisitPhase; switch (visitPhase) { - case CLONE_UPDATE: - case CLONE_UNCHANGED: - // We've found any "layout" View Transitions at this point so we can bail. - keepTraversing = false; - break; case CLONE_EXIT: case CLONE_UNHIDE: case CLONE_APPEARING_PAIR: // If this was an unhide, we need to keep going if there are any named // pairs in this subtree, since they might need to be marked. - keepTraversing = - (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags; + nextPhase = + (child.subtreeFlags & ViewTransitionNamedStatic) !== NoFlags + ? CLONE_APPEARING_PAIR + : CLONE_UNCHANGED; break; + default: + // We've found any "layout" View Transitions at this point so we can bail. + nextPhase = CLONE_UNCHANGED; } let clone: Instance; - if (keepTraversing) { + if (nextPhase !== CLONE_UNCHANGED) { // We might need a handle on these clones, so we need to do a shallow clone // and keep going. clone = cloneMutableInstance(instance, false); @@ -324,7 +324,7 @@ function recursivelyInsertClonesFromExistingTree( child, clone, null, - visitPhase, + nextPhase, ); } else { // If we have no mutations in this subtree, and we don't need a handle on the From d21b8cc6585c4a9f12ea050e5dec8de4e70dd6c5 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 13 Mar 2025 00:46:56 -0400 Subject: [PATCH 12/14] We can only bail if we don't have a parent to insert clones into Once we reach a HostComponent this will turn null and we can bail. --- packages/react-reconciler/src/ReactFiberApplyGesture.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 9411e72c1200a..fc60b6c0481e2 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -132,6 +132,7 @@ function recursivelyInsertNew( ): void { if ( visitPhase === INSERT_APPEARING_PAIR && + parentViewTransition === null && (parentFiber.subtreeFlags & ViewTransitionNamedStatic) === NoFlags ) { // We're just searching for pairs but we have reached the end. From 94bc8bcd576d5ada1c6f819503b3f7358b3d9c8a Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 13 Mar 2025 00:54:00 -0400 Subject: [PATCH 13/14] Adding to the parent view transition needs to be done even for inserted pairs --- .../react-reconciler/src/ReactFiberApplyGesture.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index fc60b6c0481e2..02876c3ea6504 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -202,13 +202,6 @@ function recursivelyInsertNewFiber( // For insertions we don't need to clone. It's already new state node. if (visitPhase !== INSERT_APPEARING_PAIR) { appendChild(hostParentClone, instance); - if (parentViewTransition !== null) { - if (parentViewTransition.clones === null) { - parentViewTransition.clones = [instance]; - } else { - parentViewTransition.clones.push(instance); - } - } recursivelyInsertNew( finishedWork, instance, @@ -218,6 +211,13 @@ function recursivelyInsertNewFiber( } else { recursivelyInsertNew(finishedWork, instance, null, visitPhase); } + if (parentViewTransition !== null) { + if (parentViewTransition.clones === null) { + parentViewTransition.clones = [instance]; + } else { + parentViewTransition.clones.push(instance); + } + } break; } case HostText: { From 59a95478c80623e089e9c0e1f3e348de72ae788b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Thu, 13 Mar 2025 20:19:25 -0400 Subject: [PATCH 14/14] Rename measureEnterViewTransitions to measureExitViewTransitions Since this is reversed. Also adding a stub for the missing measureUpdateViewTransition. --- .../src/ReactFiberApplyGesture.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 02876c3ea6504..a83c52c57cad8 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -767,7 +767,7 @@ function applyEnterViewTransitions(deletion: Fiber): void { } } -function measureEnterViewTransitions(placement: Fiber): void { +function measureExitViewTransitions(placement: Fiber): void { if (placement.tag === ViewTransitionComponent) { // const state: ViewTransitionState = placement.stateNode; const props: ViewTransitionProps = placement.memoizedProps; @@ -779,7 +779,7 @@ function measureEnterViewTransitions(placement: Fiber): void { // TODO: Check if this is a hidden Offscreen or a Portal. let child = placement.child; while (child !== null) { - measureEnterViewTransitions(child); + measureExitViewTransitions(child); child = child.sibling; } } else { @@ -806,6 +806,13 @@ function measureNestedViewTransitions(changedParent: Fiber): void { } } +function measureUpdateViewTransition( + current: Fiber, + finishedWork: Fiber, +): void { + // TODO +} + function recursivelyApplyViewTransitions(parentFiber: Fiber) { const deletions = parentFiber.deletions; if (deletions !== null) { @@ -836,7 +843,7 @@ function recursivelyApplyViewTransitions(parentFiber: Fiber) { function applyViewTransitionsOnFiber(finishedWork: Fiber) { const current = finishedWork.alternate; if (current === null) { - measureEnterViewTransitions(finishedWork); + measureExitViewTransitions(finishedWork); return; } @@ -863,7 +870,7 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { const newState: OffscreenState | null = finishedWork.memoizedState; const isHidden = newState !== null; if (!isHidden) { - measureEnterViewTransitions(finishedWork); + measureExitViewTransitions(finishedWork); } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. applyEnterViewTransitions(current); @@ -872,6 +879,7 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { break; } case ViewTransitionComponent: + measureUpdateViewTransition(current, finishedWork); const viewTransitionState: ViewTransitionState = finishedWork.stateNode; viewTransitionState.clones = null; // Reset recursivelyApplyViewTransitions(finishedWork);