From 357e93fbd11021662bf0ef101ec00029f6eaf668 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 16 Mar 2025 23:33:37 -0400 Subject: [PATCH 1/7] Fix minor issues with UpdateViewTransition path The restore phase should not check the flag. I removed that condition in the other passes. We stash previous measurments on "current". We should also clear that same Fiber. Also, when we cancel we need to visit the same Fiber we applied names to in case it changed. In practice this doesn't matter because if it did change we couldn't cancel it. --- .../src/ReactFiberCommitViewTransitions.js | 4 ++-- packages/react-reconciler/src/ReactFiberCommitWork.js | 10 +--------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 80e190181086b..97a9fb179fc5d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -564,7 +564,7 @@ export function restoreUpdateViewTransition( current: Fiber, finishedWork: Fiber, ): void { - finishedWork.memoizedState = null; + current.memoizedState = null; restoreViewTransitionOnHostInstances(current.child, true); restoreViewTransitionOnHostInstances(finishedWork.child, true); } @@ -807,7 +807,7 @@ export function measureUpdateViewTransition( if (layoutClassName === 'none') { // If we did not update, then all changes are considered a layout. We'll // attempt to cancel. - cancelViewTransitionHostInstances(finishedWork.child, oldName, true); + cancelViewTransitionHostInstances(current.child, oldName, true); return false; } // We didn't update but we might still apply layout so we measure each diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 3e2a5153dd147..5422547a7a0a6 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -3618,15 +3618,7 @@ function commitPassiveMountOnFiber( if (current === null) { // This is a new mount. We should have handled this as part of the // Placement effect or it is deeper inside a entering transition. - } else if ( - (finishedWork.subtreeFlags & - (Placement | - Update | - ChildDeletion | - ContentReset | - Visibility)) !== - NoFlags - ) { + } else { // Something mutated within this subtree. This might have caused // something to cross-fade if we didn't already cancel it. // If not, restore it. From 1eeb38eaebccc987b619e8ce74bfc22ebd6e0a50 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Fri, 14 Mar 2025 12:36:14 -0400 Subject: [PATCH 2/7] Measure updated ViewTransition boundaries to see if they could be cancelled If not cancelled, this is the path that adds the "new" name. Since the "old" name was only added to the clones, this will be the first time they're added, even if the HostInstances are shared. --- .../src/ReactFiberApplyGesture.js | 78 ++++++++++++++----- .../src/ReactFiberCommitViewTransitions.js | 35 ++++++--- .../src/ReactFiberCommitWork.js | 6 +- 3 files changed, 89 insertions(+), 30 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index c9b1d7688f580..81ab3a6b4ae18 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -48,6 +48,7 @@ import { Visibility, ViewTransitionNamedStatic, ViewTransitionStatic, + AffectedParentLayout, } from './ReactFiberFlags'; import { HostComponent, @@ -64,6 +65,10 @@ import { appearingViewTransitions, commitEnterViewTransitions, measureNestedViewTransitions, + measureUpdateViewTransition, + viewTransitionCancelableChildren, + pushViewTransitionCancelableScope, + popViewTransitionCancelableScope, } from './ReactFiberCommitViewTransitions'; import { getViewTransitionName, @@ -72,6 +77,10 @@ import { let didWarnForRootClone = false; +// Used during the apply phase to track whether a parent ViewTransition component +// might have been affected by any mutations / relayouts below. +let viewTransitionContextChanged: boolean = false; + function detectMutationOrInsertClones(finishedWork: Fiber): boolean { return true; } @@ -991,13 +1000,6 @@ function measureExitViewTransitions(placement: Fiber): void { } } -function measureUpdateViewTransition( - current: Fiber, - finishedWork: Fiber, -): void { - // TODO -} - function recursivelyApplyViewTransitions(parentFiber: Fiber) { const deletions = parentFiber.deletions; if (deletions !== null) { @@ -1037,15 +1039,6 @@ function applyViewTransitionsOnFiber(finishedWork: 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; @@ -1063,12 +1056,59 @@ function applyViewTransitionsOnFiber(finishedWork: Fiber) { } break; } - case ViewTransitionComponent: - measureUpdateViewTransition(current, finishedWork); + case ViewTransitionComponent: { + const prevContextChanged = viewTransitionContextChanged; + const prevCancelableChildren = pushViewTransitionCancelableScope(); + viewTransitionContextChanged = false; + recursivelyApplyViewTransitions(finishedWork); + + if (viewTransitionContextChanged) { + finishedWork.flags |= Update; + } + + const inViewport = measureUpdateViewTransition( + current, + finishedWork, + true, + ); + + if ((finishedWork.flags & Update) === NoFlags || !inViewport) { + // If this boundary didn't update, then we may be able to cancel its children. + // We bubble them up to the parent set to be determined later if we can cancel. + // Similarly, if old and new state was outside the viewport, we can skip it + // even if it did update. + if (prevCancelableChildren === null) { + // Bubbling up this whole set to the parent. + } else { + // Merge with parent set. + // $FlowFixMe[method-unbinding] + prevCancelableChildren.push.apply( + prevCancelableChildren, + viewTransitionCancelableChildren, + ); + popViewTransitionCancelableScope(prevCancelableChildren); + } + // TODO: If this doesn't end up canceled, because a parent animates, + // then we should probably issue an event since this instance is part of it. + } else { + // TODO: Schedule gesture events. + // If this boundary did update, we cannot cancel its children so those are dropped. + popViewTransitionCancelableScope(prevCancelableChildren); + } + + if ((finishedWork.flags & AffectedParentLayout) !== NoFlags) { + // This boundary changed size in a way that may have caused its parent to + // relayout. We need to bubble this information up to the parent. + viewTransitionContextChanged = true; + } else { + // Otherwise, we restore it to whatever the parent had found so far. + viewTransitionContextChanged = prevContextChanged; + } + const viewTransitionState: ViewTransitionState = finishedWork.stateNode; viewTransitionState.clones = null; // Reset - recursivelyApplyViewTransitions(finishedWork); break; + } default: { recursivelyApplyViewTransitions(finishedWork); break; diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 97a9fb179fc5d..ddcac06e640d1 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -774,13 +774,16 @@ function measureViewTransitionHostInstancesRecursive( export function measureUpdateViewTransition( current: Fiber, finishedWork: Fiber, + gesture: boolean, ): boolean { - const props: ViewTransitionProps = finishedWork.memoizedProps; - const newName = getViewTransitionName(props, finishedWork.stateNode); - const oldName = getViewTransitionName( - current.memoizedProps, - current.stateNode, - ); + // If this was a gesture then which Fiber was used for the "old" vs "new" state is reversed. + // We still need to treat "finishedWork" as the Fiber that contains the flags for this commmit. + const oldFiber = gesture ? finishedWork : current; + const newFiber = gesture ? current : finishedWork; + const props: ViewTransitionProps = newFiber.memoizedProps; + const state: ViewTransitionState = newFiber.stateNode; + const newName = getViewTransitionName(props, state); + const oldName = getViewTransitionName(oldFiber.memoizedProps, state); const updateClassName: ?string = getViewTransitionClassName( props.className, props.update, @@ -807,7 +810,9 @@ export function measureUpdateViewTransition( if (layoutClassName === 'none') { // If we did not update, then all changes are considered a layout. We'll // attempt to cancel. - cancelViewTransitionHostInstances(current.child, oldName, true); + // This should use the Fiber that got names applied in the snapshot phase + // since those are the ones we're trying to cancel. + cancelViewTransitionHostInstances(oldFiber.child, oldName, true); return false; } // We didn't update but we might still apply layout so we measure each @@ -816,10 +821,20 @@ export function measureUpdateViewTransition( } // If nothing changed due to a mutation, or children changing size // and the measurements end up unchanged, we should restore it to not animate. - const previousMeasurements = current.memoizedState; + let previousMeasurements: null | Array; + if (gesture) { + const clones = state.clones; + if (clones === null) { + previousMeasurements = null; + } else { + previousMeasurements = clones.map(measureInstance); + } + } else { + previousMeasurements = oldFiber.memoizedState; + } const inViewport = measureViewTransitionHostInstances( - finishedWork, - finishedWork.child, + finishedWork, // This is always finishedWork since it's used to assign flags. + newFiber.child, // This either current or finishedWork depending on if was a gesture. newName, oldName, className, diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 5422547a7a0a6..25659ae7a1acb 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2528,7 +2528,11 @@ function commitAfterMutationEffectsOnFiber( finishedWork.flags |= Update; } - const inViewport = measureUpdateViewTransition(current, finishedWork); + const inViewport = measureUpdateViewTransition( + current, + finishedWork, + false, + ); if ((finishedWork.flags & Update) === NoFlags || !inViewport) { // If this boundary didn't update, then we may be able to cancel its children. From 4c841a1375bbda31b7437ccaa1c1c1f7c57fcfb0 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 16 Mar 2025 23:29:25 -0400 Subject: [PATCH 3/7] Restore names of updated view transition boundaries --- .../react-reconciler/src/ReactFiberApplyGesture.js | 13 ++----------- .../src/ReactFiberCommitViewTransitions.js | 9 +++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 81ab3a6b4ae18..3eb5ca5b4e6fd 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -62,6 +62,7 @@ import { import { restoreEnterOrExitViewTransitions, restoreNestedViewTransitions, + restoreUpdateViewTransitionForGesture, appearingViewTransitions, commitEnterViewTransitions, measureNestedViewTransitions, @@ -1170,15 +1171,6 @@ function restoreViewTransitionsOnFiber(finishedWork: 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; @@ -1197,8 +1189,7 @@ function restoreViewTransitionsOnFiber(finishedWork: Fiber) { break; } case ViewTransitionComponent: - const viewTransitionState: ViewTransitionState = finishedWork.stateNode; - viewTransitionState.clones = null; // Reset + restoreUpdateViewTransitionForGesture(current, finishedWork); recursivelyRestoreViewTransitions(finishedWork); break; default: { diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index ddcac06e640d1..8a1b32b7af30d 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -569,6 +569,15 @@ export function restoreUpdateViewTransition( restoreViewTransitionOnHostInstances(finishedWork.child, true); } +export function restoreUpdateViewTransitionForGesture( + current: Fiber, + finishedWork: Fiber, +): void { + // For gestures we don't need to reset "finishedWork" because those would + // have all been clones that got deleted. + restoreViewTransitionOnHostInstances(current.child, true); +} + export function restoreNestedViewTransitions(changedParent: Fiber): void { let child = changedParent.child; while (child !== null) { From 844e516e8f49f1080cb29da0a373e2ff97c7da9f Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sun, 16 Mar 2025 23:42:22 -0400 Subject: [PATCH 4/7] Clear memoizedState after we use it We don't need to hold onto it all the way to the passive commit. --- .../react-reconciler/src/ReactFiberCommitViewTransitions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 8a1b32b7af30d..647ae1fbc2498 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -564,7 +564,6 @@ export function restoreUpdateViewTransition( current: Fiber, finishedWork: Fiber, ): void { - current.memoizedState = null; restoreViewTransitionOnHostInstances(current.child, true); restoreViewTransitionOnHostInstances(finishedWork.child, true); } @@ -582,7 +581,6 @@ export function restoreNestedViewTransitions(changedParent: Fiber): void { let child = changedParent.child; while (child !== null) { if (child.tag === ViewTransitionComponent) { - child.memoizedState = null; restoreViewTransitionOnHostInstances(child.child, false); } else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) { restoreNestedViewTransitions(child); @@ -840,6 +838,7 @@ export function measureUpdateViewTransition( } } else { previousMeasurements = oldFiber.memoizedState; + oldFiber.memoizedState = null; // Clear it. We won't need it anymore. } const inViewport = measureViewTransitionHostInstances( finishedWork, // This is always finishedWork since it's used to assign flags. @@ -885,6 +884,7 @@ export function measureNestedViewTransitions( } } else { previousMeasurements = child.memoizedState; + child.memoizedState = null; // Clear it. We won't need it anymore. } const inViewport = measureViewTransitionHostInstances( child, From 6ec7ae39845fd858efbf67bc94f4ee547cd4edd6 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 17 Mar 2025 00:45:04 -0400 Subject: [PATCH 5/7] Cancel any cancelable children if the root didn't change Don't update the button in the fixture optimistically to highlight this. --- .../view-transition/src/components/Page.js | 4 +-- .../src/ReactFiberApplyGesture.js | 28 ++++++++++++++++++- .../src/ReactFiberCommitWork.js | 2 +- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 19313a99e372d..48288da25234f 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -77,9 +77,9 @@ export default function Page({url, navigate}) {
diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index 3eb5ca5b4e6fd..a528033cc38f7 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -9,7 +9,7 @@ import type {Fiber, FiberRoot} from './ReactInternalTypes'; -import type {Instance, TextInstance} from './ReactFiberConfig'; +import type {Instance, TextInstance, Props} from './ReactFiberConfig'; import type {OffscreenState} from './ReactFiberActivityComponent'; @@ -25,6 +25,7 @@ import { removeRootViewTransitionClone, cancelRootViewTransitionName, restoreRootViewTransitionName, + cancelViewTransitionName, applyViewTransitionName, appendChild, commitUpdate, @@ -1123,13 +1124,38 @@ export function applyDepartureTransitions( finishedWork: Fiber, ): void { // First measure and apply view-transition-names to the "new" states. + viewTransitionContextChanged = false; + pushViewTransitionCancelableScope(); + recursivelyApplyViewTransitions(finishedWork); + // Then remove the clones. const rootClone = root.gestureClone; if (rootClone !== null) { root.gestureClone = null; removeRootViewTransitionClone(root.containerInfo, rootClone); } + + if (!viewTransitionContextChanged) { + // If we didn't leak any resizing out to the root, we don't have to transition + // the root itself. This means that we can now safely cancel any cancellations + // that bubbled all the way up. + const cancelableChildren = viewTransitionCancelableChildren; + if (cancelableChildren !== null) { + for (let i = 0; i < cancelableChildren.length; i += 3) { + cancelViewTransitionName( + ((cancelableChildren[i]: any): Instance), + ((cancelableChildren[i + 1]: any): string), + ((cancelableChildren[i + 2]: any): Props), + ); + } + } + // We also cancel the root itself. First we restore the name to the documentElement + // and then we cancel it. + restoreRootViewTransitionName(root.containerInfo); + cancelRootViewTransitionName(root.containerInfo); + } + popViewTransitionCancelableScope(null); } function recursivelyRestoreViewTransitions(parentFiber: Fiber) { diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 25659ae7a1acb..63bf7b7eb9b15 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2474,7 +2474,6 @@ function commitAfterMutationEffectsOnFiber( // the root itself. This means that we can now safely cancel any cancellations // that bubbled all the way up. const cancelableChildren = viewTransitionCancelableChildren; - popViewTransitionCancelableScope(null); if (cancelableChildren !== null) { for (let i = 0; i < cancelableChildren.length; i += 3) { cancelViewTransitionName( @@ -2487,6 +2486,7 @@ function commitAfterMutationEffectsOnFiber( // We also cancel the root itself. cancelRootViewTransitionName(root.containerInfo); } + popViewTransitionCancelableScope(null); break; } case HostComponent: { From a3e0ee89869474ad36d887258c1c1fa58e82f25c Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 17 Mar 2025 12:30:54 -0400 Subject: [PATCH 6/7] Add measureClonedInstance config This allows us to adjust the measurement of the clone which is outside the viewport as if it was inside of it. --- packages/react-art/src/ReactFiberConfigART.js | 4 +++ .../src/client/ReactFiberConfigDOM.js | 30 ++++++++++++++++--- .../src/ReactFiberConfigNative.js | 4 +++ .../src/createReactNoop.js | 4 +++ .../src/ReactFiberCommitViewTransitions.js | 5 ++-- .../src/ReactFiberConfigWithNoMutation.js | 1 + .../src/forks/ReactFiberConfig.custom.js | 1 + .../src/ReactFiberConfigTestHost.js | 4 +++ 8 files changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 5b8788453f6af..a168975221f34 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -518,6 +518,10 @@ export function measureInstance(instance) { return null; } +export function measureClonedInstance(instance) { + return null; +} + export function wasInstanceInViewport(measurement): boolean { return true; } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 2d34bc94005be..3615e06481629 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1477,10 +1477,12 @@ export type InstanceMeasurement = { view: boolean, // is in viewport bounds }; -export function measureInstance(instance: Instance): InstanceMeasurement { - const ownerWindow = instance.ownerDocument.defaultView; - const rect = instance.getBoundingClientRect(); - const computedStyle = getComputedStyle(instance); +function createMeasurement( + rect: ClientRect | DOMRect, + computedStyle: CSSStyleDeclaration, + element: Element, +): InstanceMeasurement { + const ownerWindow = element.ownerDocument.defaultView; return { rect: rect, abs: @@ -1508,6 +1510,26 @@ export function measureInstance(instance: Instance): InstanceMeasurement { }; } +export function measureInstance(instance: Instance): InstanceMeasurement { + const rect = instance.getBoundingClientRect(); + const computedStyle = getComputedStyle(instance); + return createMeasurement(rect, computedStyle, instance); +} + +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + const measuredRect = instance.getBoundingClientRect(); + // Adjust the DOMRect based on the translate that put it outside the viewport. + // TODO: This might not be completely correct if the parent also has a transform. + const rect = new DOMRect( + measuredRect.x + 20000, + measuredRect.y + 20000, + measuredRect.width, + measuredRect.height, + ); + const computedStyle = getComputedStyle(instance); + return createMeasurement(rect, computedStyle, instance); +} + export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index c08e1f0f82474..b15a6f9f76b02 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -620,6 +620,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement { return null; } +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + return null; +} + export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 2ddbea79289fa..266f860fe6d32 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -796,6 +796,10 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { return null; }, + measureClonedInstance(instance: Instance): InstanceMeasurement { + return null; + }, + wasInstanceInViewport(measurement: InstanceMeasurement): boolean { return true; }, diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 647ae1fbc2498..c8ec019da7f07 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -31,6 +31,7 @@ import { applyViewTransitionName, restoreViewTransitionName, measureInstance, + measureClonedInstance, hasInstanceChanged, hasInstanceAffectedParent, wasInstanceInViewport, @@ -834,7 +835,7 @@ export function measureUpdateViewTransition( if (clones === null) { previousMeasurements = null; } else { - previousMeasurements = clones.map(measureInstance); + previousMeasurements = clones.map(measureClonedInstance); } } else { previousMeasurements = oldFiber.memoizedState; @@ -880,7 +881,7 @@ export function measureNestedViewTransitions( if (clones === null) { previousMeasurements = null; } else { - previousMeasurements = clones.map(measureInstance); + previousMeasurements = clones.map(measureClonedInstance); } } else { previousMeasurements = child.memoizedState; diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js index 64b67491aa8d5..74e30da88c96e 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js @@ -46,6 +46,7 @@ export const cloneRootViewTransitionContainer = shim; export const removeRootViewTransitionClone = shim; export type InstanceMeasurement = null; export const measureInstance = shim; +export const measureClonedInstance = shim; export const wasInstanceInViewport = shim; export const hasInstanceChanged = shim; export const hasInstanceAffectedParent = shim; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 2be7d18b87caf..f22b6a580e7d9 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -149,6 +149,7 @@ export const cloneRootViewTransitionContainer = export const removeRootViewTransitionClone = $$$config.removeRootViewTransitionClone; export const measureInstance = $$$config.measureInstance; +export const measureClonedInstance = $$$config.measureClonedInstance; export const wasInstanceInViewport = $$$config.wasInstanceInViewport; export const hasInstanceChanged = $$$config.hasInstanceChanged; export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent; diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index d9a45550fa4b2..2df5f0157195c 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -389,6 +389,10 @@ export function measureInstance(instance: Instance): InstanceMeasurement { return null; } +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + return null; +} + export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { From 0f4abc984acf63435dda3339ea0d3bb329050478 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 17 Mar 2025 18:00:47 -0400 Subject: [PATCH 7/7] Track Host Mutations in Apply Gesture Because we don't use the helpers in CommitHostEffects we have to manually track whether this caused any mutations for deletions, insertions, hides, unhides and updates to text content. Otherwise, we end up canceling boundaries that should've animated. --- .../src/ReactFiberApplyGesture.js | 17 +++++++++++++++++ .../src/ReactFiberCommitHostEffects.js | 1 + 2 files changed, 18 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberApplyGesture.js b/packages/react-reconciler/src/ReactFiberApplyGesture.js index a528033cc38f7..ea4a0ce70ae5f 100644 --- a/packages/react-reconciler/src/ReactFiberApplyGesture.js +++ b/packages/react-reconciler/src/ReactFiberApplyGesture.js @@ -40,6 +40,7 @@ import { popMutationContext, pushMutationContext, viewTransitionMutationContext, + trackHostMutation, } from './ReactFiberMutationTracking'; import { MutationMask, @@ -432,6 +433,7 @@ function recursivelyInsertNewFiber( // For insertions we don't need to clone. It's already new state node. if (visitPhase !== INSERT_APPEARING_PAIR) { appendChild(hostParentClone, instance); + trackHostMutation(); recursivelyInsertNew( finishedWork, instance, @@ -461,6 +463,7 @@ function recursivelyInsertNewFiber( // For insertions we don't need to clone. It's already new state node. if (visitPhase !== INSERT_APPEARING_PAIR) { appendChild(hostParentClone, textInstance); + trackHostMutation(); } break; } @@ -586,6 +589,7 @@ function recursivelyInsertClonesFromExistingTree( } if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { unhideInstance(clone, child.memoizedProps); + trackHostMutation(); } break; } @@ -601,6 +605,7 @@ function recursivelyInsertClonesFromExistingTree( appendChild(hostParentClone, clone); if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { unhideTextInstance(clone, child.memoizedProps); + trackHostMutation(); } break; } @@ -690,6 +695,10 @@ function recursivelyInsertClones( for (let i = 0; i < deletions.length; i++) { const childToDelete = deletions[i]; trackEnterViewTransitions(childToDelete); + // Normally we would only mark something as triggering a mutation if there was + // actually a HostInstance below here. If this tree didn't contain a HostInstances + // we shouldn't trigger a mutation even though a virtual component was deleted. + trackHostMutation(); } } @@ -812,6 +821,7 @@ function insertDestinationClonesOfFiber( clone = cloneMutableInstance(instance, true); if (finishedWork.flags & ContentReset) { resetTextContent(clone); + trackHostMutation(); } } else { // If we have children we'll clone them as we walk the tree so we just @@ -836,6 +846,7 @@ function insertDestinationClonesOfFiber( ); appendChild(hostParentClone, clone); unhideInstance(clone, finishedWork.memoizedProps); + trackHostMutation(); } else { recursivelyInsertClones(finishedWork, clone, null, visitPhase); appendChild(hostParentClone, clone); @@ -862,10 +873,12 @@ function insertDestinationClonesOfFiber( const newText: string = finishedWork.memoizedProps; const oldText: string = current.memoizedProps; commitTextUpdate(clone, newText, oldText); + trackHostMutation(); } appendChild(hostParentClone, clone); if (visitPhase === CLONE_EXIT || visitPhase === CLONE_UNHIDE) { unhideTextInstance(clone, finishedWork.memoizedProps); + trackHostMutation(); } break; } @@ -896,6 +909,10 @@ function insertDestinationClonesOfFiber( } else if (current !== null && current.memoizedState === null) { // Was previously mounted as visible but is now hidden. trackEnterViewTransitions(current); + // Normally we would only mark something as triggering a mutation if there was + // actually a HostInstance below here. If this tree didn't contain a HostInstances + // we shouldn't trigger a mutation even though a virtual component was hidden. + trackHostMutation(); } break; } diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js index 7dad8b330d7e2..2ca49f677de1f 100644 --- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js @@ -199,6 +199,7 @@ export function commitShowHideHostTextInstance(node: Fiber, isHidden: boolean) { unhideTextInstance(instance, node.memoizedProps); } } + trackHostMutation(); } catch (error) { captureCommitPhaseError(node, node.return, error); }