From fa509c1e8479c7bd99ac78f1871fdd2356b70755 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Mar 2026 15:35:31 -0700 Subject: [PATCH 01/30] Add enableViewTransitionForPersistenceMode feature flag Wherever the reconciler checks supportsMutation before running view transition logic, add an else-if branch gated on enableViewTransitionForPersistenceMode for persistent renderers (Fabric). This follows the pattern that supportsMutation guards mutation-mode-only logic, and persistent mode should have its own branch rather than being lumped under a single supportsViewTransition capability flag. For now the persistent mode branches duplicate the mutation logic. The flag defaults to false in all channels. --- .../src/ReactFiberCommitViewTransitions.js | 486 ++++++++++++------ .../src/ReactFiberCommitWork.js | 6 + packages/shared/ReactFeatureFlags.js | 2 + .../forks/ReactFeatureFlags.native-fb.js | 1 + .../forks/ReactFeatureFlags.native-oss.js | 1 + .../forks/ReactFeatureFlags.test-renderer.js | 1 + ...actFeatureFlags.test-renderer.native-fb.js | 1 + .../ReactFeatureFlags.test-renderer.www.js | 1 + .../shared/forks/ReactFeatureFlags.www.js | 2 + 9 files changed, 338 insertions(+), 163 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index a9edc0c84d23..5a6b243f1f35 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -34,6 +34,7 @@ import { hasInstanceAffectedParent, wasInstanceInViewport, } from './ReactFiberConfig'; +import {enableViewTransitionForPersistenceMode} from 'shared/ReactFeatureFlags'; import { scheduleViewTransitionEvent, scheduleGestureTransitionEvent, @@ -139,93 +140,168 @@ function applyViewTransitionToHostInstancesRecursive( collectMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (!supportsMutation) { - return false; - } - let inViewport = false; - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if (collectMeasurements !== null) { - const measurement = measureInstance(instance); - collectMeasurements.push(measurement); - if (wasInstanceInViewport(measurement)) { - inViewport = true; + if (supportsMutation) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if (collectMeasurements !== null) { + const measurement = measureInstance(instance); + collectMeasurements.push(measurement); + if (wasInstanceInViewport(measurement)) { + inViewport = true; + } + } else if (!inViewport) { + if (wasInstanceInViewport(measureInstance(instance))) { + inViewport = true; + } } - } else if (!inViewport) { - if (wasInstanceInViewport(measureInstance(instance))) { + shouldStartViewTransition = true; + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? name + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + name + '_' + viewTransitionHostInstanceIdx, + className, + ); + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + if ( + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ) + ) { inViewport = true; } } - shouldStartViewTransition = true; - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? name - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - name + '_' + viewTransitionHostInstanceIdx, - className, - ); - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - if ( - applyViewTransitionToHostInstancesRecursive( - child.child, - name, + child = child.sibling; + } + return inViewport; + } else if (enableViewTransitionForPersistenceMode) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if (collectMeasurements !== null) { + const measurement = measureInstance(instance); + collectMeasurements.push(measurement); + if (wasInstanceInViewport(measurement)) { + inViewport = true; + } + } else if (!inViewport) { + if (wasInstanceInViewport(measureInstance(instance))) { + inViewport = true; + } + } + shouldStartViewTransition = true; + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? name + : name + '_' + viewTransitionHostInstanceIdx, className, - collectMeasurements, - stopAtNestedViewTransitions, - ) + ); + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null ) { - inViewport = true; + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + if ( + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ) + ) { + inViewport = true; + } } + child = child.sibling; } - child = child.sibling; + return inViewport; } - return inViewport; + return false; } function restoreViewTransitionOnHostInstances( child: null | Fiber, stopAtNestedViewTransitions: boolean, ): void { - if (!supportsMutation) { - return; - } - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - restoreViewTransitionName(instance, child.memoizedProps); - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - restoreViewTransitionOnHostInstances( - child.child, - stopAtNestedViewTransitions, - ); + if (supportsMutation) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + restoreViewTransitionName(instance, child.memoizedProps); + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + restoreViewTransitionOnHostInstances( + child.child, + stopAtNestedViewTransitions, + ); + } + child = child.sibling; + } + } else if (enableViewTransitionForPersistenceMode) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + restoreViewTransitionName(instance, child.memoizedProps); + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + restoreViewTransitionOnHostInstances( + child.child, + stopAtNestedViewTransitions, + ); + } + child = child.sibling; } - child = child.sibling; } } @@ -648,112 +724,196 @@ function measureViewTransitionHostInstancesRecursive( previousMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (!supportsMutation) { - return true; - } - let inViewport = false; - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if ( - previousMeasurements !== null && - viewTransitionHostInstanceIdx < previousMeasurements.length + if (supportsMutation) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if ( + previousMeasurements !== null && + viewTransitionHostInstanceIdx < previousMeasurements.length + ) { + // The previous measurement of the Instance in this location within the ViewTransition. + // Note that this might not be the same exact Instance if the Instances within the + // ViewTransition changed. + const previousMeasurement = + previousMeasurements[viewTransitionHostInstanceIdx]; + const nextMeasurement = measureInstance(instance); + if ( + wasInstanceInViewport(previousMeasurement) || + wasInstanceInViewport(nextMeasurement) + ) { + // If either the old or new state was within the viewport we have to animate this. + // But if it turns out that none of them were we'll be able to skip it. + inViewport = true; + } + if ( + (parentViewTransition.flags & Update) === NoFlags && + hasInstanceChanged(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= Update; + } + if ( + hasInstanceAffectedParent(previousMeasurement, nextMeasurement) + ) { + // If this instance size within its parent has changed it might have caused the + // parent to relayout which needs a cross fade. + parentViewTransition.flags |= AffectedParentLayout; + } + } else { + // If there was an insertion of extra nodes, we have to assume they affected the parent. + // It should have already been marked as an Update due to the mutation. + parentViewTransition.flags |= AffectedParentLayout; + } + if ((parentViewTransition.flags & Update) !== NoFlags) { + // We might update this node so we need to apply its new name for the new state. + // Additionally in the ApplyGesture case we also need to do this because the clone + // will have the name but this one won't. + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? newName + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + newName + '_' + viewTransitionHostInstanceIdx, + className, + ); + } + if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { + // It turns out that we had no other deeper mutations, the child transitions didn't + // affect the parent layout and this instance hasn't changed size. So we can skip + // animating it. However, in the current model this only works if the parent also + // doesn't animate. So we have to queue these and wait until we complete the parent + // to cancel them. + if (viewTransitionCancelableChildren === null) { + viewTransitionCancelableChildren = []; + } + viewTransitionCancelableChildren.push( + instance, + viewTransitionHostInstanceIdx === 0 + ? oldName + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + oldName + '_' + viewTransitionHostInstanceIdx, + child.memoizedProps, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null ) { - // The previous measurement of the Instance in this location within the ViewTransition. - // Note that this might not be the same exact Instance if the Instances within the - // ViewTransition changed. - const previousMeasurement = - previousMeasurements[viewTransitionHostInstanceIdx]; - const nextMeasurement = measureInstance(instance); + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + // If this inner boundary resized we need to bubble that information up. + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { if ( - wasInstanceInViewport(previousMeasurement) || - wasInstanceInViewport(nextMeasurement) + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, + className, + previousMeasurements, + stopAtNestedViewTransitions, + ) ) { - // If either the old or new state was within the viewport we have to animate this. - // But if it turns out that none of them were we'll be able to skip it. inViewport = true; } + } + child = child.sibling; + } + return inViewport; + } else if (enableViewTransitionForPersistenceMode) { + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; if ( - (parentViewTransition.flags & Update) === NoFlags && - hasInstanceChanged(previousMeasurement, nextMeasurement) + previousMeasurements !== null && + viewTransitionHostInstanceIdx < previousMeasurements.length ) { - parentViewTransition.flags |= Update; - } - if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) { - // If this instance size within its parent has changed it might have caused the - // parent to relayout which needs a cross fade. + const previousMeasurement = + previousMeasurements[viewTransitionHostInstanceIdx]; + const nextMeasurement = measureInstance(instance); + if ( + wasInstanceInViewport(previousMeasurement) || + wasInstanceInViewport(nextMeasurement) + ) { + inViewport = true; + } + if ( + (parentViewTransition.flags & Update) === NoFlags && + hasInstanceChanged(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= Update; + } + if ( + hasInstanceAffectedParent(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= AffectedParentLayout; + } + } else { parentViewTransition.flags |= AffectedParentLayout; } - } else { - // If there was an insertion of extra nodes, we have to assume they affected the parent. - // It should have already been marked as an Update due to the mutation. - parentViewTransition.flags |= AffectedParentLayout; - } - if ((parentViewTransition.flags & Update) !== NoFlags) { - // We might update this node so we need to apply its new name for the new state. - // Additionally in the ApplyGesture case we also need to do this because the clone - // will have the name but this one won't. - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? newName - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - newName + '_' + viewTransitionHostInstanceIdx, - className, - ); - } - if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { - // It turns out that we had no other deeper mutations, the child transitions didn't - // affect the parent layout and this instance hasn't changed size. So we can skip - // animating it. However, in the current model this only works if the parent also - // doesn't animate. So we have to queue these and wait until we complete the parent - // to cancel them. - if (viewTransitionCancelableChildren === null) { - viewTransitionCancelableChildren = []; + if ((parentViewTransition.flags & Update) !== NoFlags) { + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? newName + : newName + '_' + viewTransitionHostInstanceIdx, + className, + ); } - viewTransitionCancelableChildren.push( - instance, - viewTransitionHostInstanceIdx === 0 - ? oldName - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - oldName + '_' + viewTransitionHostInstanceIdx, - child.memoizedProps, - ); - } - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - // If this inner boundary resized we need to bubble that information up. - parentViewTransition.flags |= child.flags & AffectedParentLayout; - } else { - if ( - measureViewTransitionHostInstancesRecursive( - parentViewTransition, - child.child, - newName, - oldName, - className, - previousMeasurements, - stopAtNestedViewTransitions, - ) + if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { + if (viewTransitionCancelableChildren === null) { + viewTransitionCancelableChildren = []; + } + viewTransitionCancelableChildren.push( + instance, + viewTransitionHostInstanceIdx === 0 + ? oldName + : oldName + '_' + viewTransitionHostInstanceIdx, + child.memoizedProps, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions ) { - inViewport = true; + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { + if ( + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, + className, + previousMeasurements, + stopAtNestedViewTransitions, + ) + ) { + inViewport = true; + } } + child = child.sibling; } - child = child.sibling; + return inViewport; } - return inViewport; + return true; } export function measureUpdateViewTransition( diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 322c858bb9c0..2b356f721822 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -58,6 +58,7 @@ import { disableLegacyMode, enableComponentPerformanceTrack, enableViewTransition, + enableViewTransitionForPersistenceMode, enableFragmentRefs, enableEagerAlternateStateNodeCleanup, enableDefaultTransitionIndicator, @@ -3714,6 +3715,11 @@ function commitPassiveMountOnFiber( if (isViewTransitionEligible) { if (supportsMutation && rootViewTransitionNameCanceled) { restoreRootViewTransitionName(finishedRoot.containerInfo); + } else if ( + enableViewTransitionForPersistenceMode && + rootViewTransitionNameCanceled + ) { + restoreRootViewTransitionName(finishedRoot.containerInfo); } } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index a54483df456e..ba4a7c52e3d3 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -80,6 +80,8 @@ export const enableTaint = __EXPERIMENTAL__; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; + export const enableGestureTransition = __EXPERIMENTAL__; export const enableScrollEndPolyfill = __EXPERIMENTAL__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 3f25d8028c98..db97d4b19b1c 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -69,6 +69,7 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 1c34990f5814..035bf2a75dd0 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -59,6 +59,7 @@ export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 40b6926c3572..76597e0cbb01 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -60,6 +60,7 @@ export const enableYieldingBeforePassive: boolean = true; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index a429ef71318a..8022dd8e2254 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -55,6 +55,7 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive = false; export const enableThrottledScheduling = false; export const enableViewTransition = true; +export const enableViewTransitionForPersistenceMode = false; export const enableGestureTransition = false; export const enableScrollEndPolyfill = true; export const enableSuspenseyImages = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index cebd5b5608d0..271c464daa60 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -66,6 +66,7 @@ export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 76e3909ccafc..a07f34414217 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -98,6 +98,8 @@ export const disableLegacyMode: boolean = true; export const enableEagerAlternateStateNodeCleanup: boolean = true; +export const enableViewTransitionForPersistenceMode: boolean = false; + export const enableGestureTransition: boolean = false; export const enableSuspenseyImages: boolean = false; From f2919589c954a1a29f98c886a7eae2ad5feb4d4e Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 18 Mar 2026 18:12:15 -0400 Subject: [PATCH 02/30] Simplify persistence mode view transition logic Remove viewport measurement and cancelable children tracking for persistence mode paths, deferring viewport calculation to a future TODO. Remove duplicate restoreRootViewTransitionName call and deduplicate feature flag import. --- .../src/ReactFiberCommitViewTransitions.js | 113 ++++-------------- .../src/ReactFiberCommitWork.js | 6 - 2 files changed, 20 insertions(+), 99 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 5a6b243f1f35..b7d5904a8442 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -34,7 +34,6 @@ import { hasInstanceAffectedParent, wasInstanceInViewport, } from './ReactFiberConfig'; -import {enableViewTransitionForPersistenceMode} from 'shared/ReactFeatureFlags'; import { scheduleViewTransitionEvent, scheduleGestureTransitionEvent, @@ -47,6 +46,7 @@ import {trackAnimatingTask} from './ReactProfilerTimer'; import { enableComponentPerformanceTrack, enableProfilerTimer, + enableViewTransitionForPersistenceMode, } from 'shared/ReactFeatureFlags'; export let shouldStartViewTransition: boolean = false; @@ -195,21 +195,10 @@ function applyViewTransitionToHostInstancesRecursive( } return inViewport; } else if (enableViewTransitionForPersistenceMode) { - let inViewport = false; while (child !== null) { if (child.tag === HostComponent) { const instance: Instance = child.stateNode; - if (collectMeasurements !== null) { - const measurement = measureInstance(instance); - collectMeasurements.push(measurement); - if (wasInstanceInViewport(measurement)) { - inViewport = true; - } - } else if (!inViewport) { - if (wasInstanceInViewport(measureInstance(instance))) { - inViewport = true; - } - } + // TODO: calculate whether component is in viewport shouldStartViewTransition = true; applyViewTransitionName( instance, @@ -231,21 +220,17 @@ function applyViewTransitionToHostInstancesRecursive( // Skip any nested view transitions for updates since in that case the // inner most one is the one that handles the update. } else { - if ( - applyViewTransitionToHostInstancesRecursive( - child.child, - name, - className, - collectMeasurements, - stopAtNestedViewTransitions, - ) - ) { - inViewport = true; - } + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ); } child = child.sibling; } - return inViewport; + return true; } return false; } @@ -278,30 +263,6 @@ function restoreViewTransitionOnHostInstances( } child = child.sibling; } - } else if (enableViewTransitionForPersistenceMode) { - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - restoreViewTransitionName(instance, child.memoizedProps); - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - restoreViewTransitionOnHostInstances( - child.child, - stopAtNestedViewTransitions, - ); - } - child = child.sibling; - } } } @@ -831,7 +792,6 @@ function measureViewTransitionHostInstancesRecursive( } return inViewport; } else if (enableViewTransitionForPersistenceMode) { - let inViewport = false; while (child !== null) { if (child.tag === HostComponent) { const instance: Instance = child.stateNode; @@ -842,23 +802,6 @@ function measureViewTransitionHostInstancesRecursive( const previousMeasurement = previousMeasurements[viewTransitionHostInstanceIdx]; const nextMeasurement = measureInstance(instance); - if ( - wasInstanceInViewport(previousMeasurement) || - wasInstanceInViewport(nextMeasurement) - ) { - inViewport = true; - } - if ( - (parentViewTransition.flags & Update) === NoFlags && - hasInstanceChanged(previousMeasurement, nextMeasurement) - ) { - parentViewTransition.flags |= Update; - } - if ( - hasInstanceAffectedParent(previousMeasurement, nextMeasurement) - ) { - parentViewTransition.flags |= AffectedParentLayout; - } } else { parentViewTransition.flags |= AffectedParentLayout; } @@ -871,18 +814,6 @@ function measureViewTransitionHostInstancesRecursive( className, ); } - if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { - if (viewTransitionCancelableChildren === null) { - viewTransitionCancelableChildren = []; - } - viewTransitionCancelableChildren.push( - instance, - viewTransitionHostInstanceIdx === 0 - ? oldName - : oldName + '_' + viewTransitionHostInstanceIdx, - child.memoizedProps, - ); - } viewTransitionHostInstanceIdx++; } else if ( child.tag === OffscreenComponent && @@ -895,23 +826,19 @@ function measureViewTransitionHostInstancesRecursive( ) { parentViewTransition.flags |= child.flags & AffectedParentLayout; } else { - if ( - measureViewTransitionHostInstancesRecursive( - parentViewTransition, - child.child, - newName, - oldName, - className, - previousMeasurements, - stopAtNestedViewTransitions, - ) - ) { - inViewport = true; - } + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, + className, + previousMeasurements, + stopAtNestedViewTransitions, + ); } child = child.sibling; } - return inViewport; + return true; } return true; } diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 2b356f721822..322c858bb9c0 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -58,7 +58,6 @@ import { disableLegacyMode, enableComponentPerformanceTrack, enableViewTransition, - enableViewTransitionForPersistenceMode, enableFragmentRefs, enableEagerAlternateStateNodeCleanup, enableDefaultTransitionIndicator, @@ -3715,11 +3714,6 @@ function commitPassiveMountOnFiber( if (isViewTransitionEligible) { if (supportsMutation && rootViewTransitionNameCanceled) { restoreRootViewTransitionName(finishedRoot.containerInfo); - } else if ( - enableViewTransitionForPersistenceMode && - rootViewTransitionNameCanceled - ) { - restoreRootViewTransitionName(finishedRoot.containerInfo); } } From bad3c40e5bb74accfd3db001b5ba3864bfb9a155 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 18 Mar 2026 18:17:41 -0400 Subject: [PATCH 03/30] Early return in restoreViewTransitionOnHostInstances for non-mutation mode --- .../src/ReactFiberCommitViewTransitions.js | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index b7d5904a8442..34a4a8910068 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -239,30 +239,31 @@ function restoreViewTransitionOnHostInstances( child: null | Fiber, stopAtNestedViewTransitions: boolean, ): void { - if (supportsMutation) { - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - restoreViewTransitionName(instance, child.memoizedProps); - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - restoreViewTransitionOnHostInstances( - child.child, - stopAtNestedViewTransitions, - ); - } - child = child.sibling; + if (!supportsMutation) { + return; + } + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + restoreViewTransitionName(instance, child.memoizedProps); + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + restoreViewTransitionOnHostInstances( + child.child, + stopAtNestedViewTransitions, + ); } + child = child.sibling; } } From 2e0f097f6e795b075585b7959ecb2ef3304603ef Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 18 Mar 2026 18:20:52 -0400 Subject: [PATCH 04/30] Refactor applyViewTransitionToHostInstancesRecursive to early return for persistence mode --- .../src/ReactFiberCommitViewTransitions.js | 110 +++++++++--------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 34a4a8910068..52304c2b8587 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -140,61 +140,7 @@ function applyViewTransitionToHostInstancesRecursive( collectMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (supportsMutation) { - let inViewport = false; - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if (collectMeasurements !== null) { - const measurement = measureInstance(instance); - collectMeasurements.push(measurement); - if (wasInstanceInViewport(measurement)) { - inViewport = true; - } - } else if (!inViewport) { - if (wasInstanceInViewport(measureInstance(instance))) { - inViewport = true; - } - } - shouldStartViewTransition = true; - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? name - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - name + '_' + viewTransitionHostInstanceIdx, - className, - ); - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - if ( - applyViewTransitionToHostInstancesRecursive( - child.child, - name, - className, - collectMeasurements, - stopAtNestedViewTransitions, - ) - ) { - inViewport = true; - } - } - child = child.sibling; - } - return inViewport; - } else if (enableViewTransitionForPersistenceMode) { + if (!supportsMutation && enableViewTransitionForPersistenceMode) { while (child !== null) { if (child.tag === HostComponent) { const instance: Instance = child.stateNode; @@ -232,7 +178,59 @@ function applyViewTransitionToHostInstancesRecursive( } return true; } - return false; + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if (collectMeasurements !== null) { + const measurement = measureInstance(instance); + collectMeasurements.push(measurement); + if (wasInstanceInViewport(measurement)) { + inViewport = true; + } + } else if (!inViewport) { + if (wasInstanceInViewport(measureInstance(instance))) { + inViewport = true; + } + } + shouldStartViewTransition = true; + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? name + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + name + '_' + viewTransitionHostInstanceIdx, + className, + ); + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + if ( + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ) + ) { + inViewport = true; + } + } + child = child.sibling; + } + return inViewport; } function restoreViewTransitionOnHostInstances( From b5868c7851b8e1315e7719abe8ffc093d381c814 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 08:58:53 -0400 Subject: [PATCH 05/30] Refactor measureViewTransitionHostInstancesRecursive to early return for persistence mode --- .../src/ReactFiberCommitViewTransitions.js | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 52304c2b8587..7181e78af177 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -684,7 +684,45 @@ function measureViewTransitionHostInstancesRecursive( previousMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (supportsMutation) { + if (!supportsMutation && enableViewTransitionForPersistenceMode) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if ((parentViewTransition.flags & Update) !== NoFlags) { + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? newName + : newName + '_' + viewTransitionHostInstanceIdx, + className, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, + className, + previousMeasurements, + stopAtNestedViewTransitions, + ); + } + child = child.sibling; + } + return true; + } let inViewport = false; while (child !== null) { if (child.tag === HostComponent) { @@ -790,56 +828,6 @@ function measureViewTransitionHostInstancesRecursive( child = child.sibling; } return inViewport; - } else if (enableViewTransitionForPersistenceMode) { - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if ( - previousMeasurements !== null && - viewTransitionHostInstanceIdx < previousMeasurements.length - ) { - const previousMeasurement = - previousMeasurements[viewTransitionHostInstanceIdx]; - const nextMeasurement = measureInstance(instance); - } else { - parentViewTransition.flags |= AffectedParentLayout; - } - if ((parentViewTransition.flags & Update) !== NoFlags) { - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? newName - : newName + '_' + viewTransitionHostInstanceIdx, - className, - ); - } - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - parentViewTransition.flags |= child.flags & AffectedParentLayout; - } else { - measureViewTransitionHostInstancesRecursive( - parentViewTransition, - child.child, - newName, - oldName, - className, - previousMeasurements, - stopAtNestedViewTransitions, - ); - } - child = child.sibling; - } - return true; - } - return true; } export function measureUpdateViewTransition( From 33641614436e3f002134fd8885c2e88e5b076bd3 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 09:03:08 -0400 Subject: [PATCH 06/30] Fix formatting and restructure persistence mode early returns --- .../src/ReactFiberCommitViewTransitions.js | 332 +++++++++--------- 1 file changed, 169 insertions(+), 163 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 7181e78af177..8b2dd192dd9f 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -140,43 +140,47 @@ function applyViewTransitionToHostInstancesRecursive( collectMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (!supportsMutation && enableViewTransitionForPersistenceMode) { - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - // TODO: calculate whether component is in viewport - shouldStartViewTransition = true; - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? name - : name + '_' + viewTransitionHostInstanceIdx, - className, - ); - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions - ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - } else { - applyViewTransitionToHostInstancesRecursive( - child.child, - name, - className, - collectMeasurements, - stopAtNestedViewTransitions, - ); + if (!supportsMutation) { + if (enableViewTransitionForPersistenceMode) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + // TODO: calculate whether component is in viewport + shouldStartViewTransition = true; + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? name + : name + '_' + viewTransitionHostInstanceIdx, + className, + ); + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + } else { + applyViewTransitionToHostInstancesRecursive( + child.child, + name, + className, + collectMeasurements, + stopAtNestedViewTransitions, + ); + } + child = child.sibling; } - child = child.sibling; + return true; + } else { + return false; } - return true; } let inViewport = false; while (child !== null) { @@ -684,31 +688,136 @@ function measureViewTransitionHostInstancesRecursive( previousMeasurements: null | Array, stopAtNestedViewTransitions: boolean, ): boolean { - if (!supportsMutation && enableViewTransitionForPersistenceMode) { - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if ((parentViewTransition.flags & Update) !== NoFlags) { - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? newName - : newName + '_' + viewTransitionHostInstanceIdx, + if (!supportsMutation) { + if (enableViewTransitionForPersistenceMode) { + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if ((parentViewTransition.flags & Update) !== NoFlags) { + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? newName + : newName + '_' + viewTransitionHostInstanceIdx, + className, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { + measureViewTransitionHostInstancesRecursive( + parentViewTransition, + child.child, + newName, + oldName, className, + previousMeasurements, + stopAtNestedViewTransitions, ); } - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions + child = child.sibling; + } + return true; + } else { + return false; + } + } + let inViewport = false; + while (child !== null) { + if (child.tag === HostComponent) { + const instance: Instance = child.stateNode; + if ( + previousMeasurements !== null && + viewTransitionHostInstanceIdx < previousMeasurements.length ) { - parentViewTransition.flags |= child.flags & AffectedParentLayout; + // The previous measurement of the Instance in this location within the ViewTransition. + // Note that this might not be the same exact Instance if the Instances within the + // ViewTransition changed. + const previousMeasurement = + previousMeasurements[viewTransitionHostInstanceIdx]; + const nextMeasurement = measureInstance(instance); + if ( + wasInstanceInViewport(previousMeasurement) || + wasInstanceInViewport(nextMeasurement) + ) { + // If either the old or new state was within the viewport we have to animate this. + // But if it turns out that none of them were we'll be able to skip it. + inViewport = true; + } + if ( + (parentViewTransition.flags & Update) === NoFlags && + hasInstanceChanged(previousMeasurement, nextMeasurement) + ) { + parentViewTransition.flags |= Update; + } + if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) { + // If this instance size within its parent has changed it might have caused the + // parent to relayout which needs a cross fade. + parentViewTransition.flags |= AffectedParentLayout; + } } else { + // If there was an insertion of extra nodes, we have to assume they affected the parent. + // It should have already been marked as an Update due to the mutation. + parentViewTransition.flags |= AffectedParentLayout; + } + if ((parentViewTransition.flags & Update) !== NoFlags) { + // We might update this node so we need to apply its new name for the new state. + // Additionally in the ApplyGesture case we also need to do this because the clone + // will have the name but this one won't. + applyViewTransitionName( + instance, + viewTransitionHostInstanceIdx === 0 + ? newName + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + newName + '_' + viewTransitionHostInstanceIdx, + className, + ); + } + if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { + // It turns out that we had no other deeper mutations, the child transitions didn't + // affect the parent layout and this instance hasn't changed size. So we can skip + // animating it. However, in the current model this only works if the parent also + // doesn't animate. So we have to queue these and wait until we complete the parent + // to cancel them. + if (viewTransitionCancelableChildren === null) { + viewTransitionCancelableChildren = []; + } + viewTransitionCancelableChildren.push( + instance, + viewTransitionHostInstanceIdx === 0 + ? oldName + : // If we have multiple Host Instances below, we add a suffix to the name to give + // each one a unique name. + oldName + '_' + viewTransitionHostInstanceIdx, + child.memoizedProps, + ); + } + viewTransitionHostInstanceIdx++; + } else if ( + child.tag === OffscreenComponent && + child.memoizedState !== null + ) { + // Skip any hidden subtrees. They were or are effectively not there. + } else if ( + child.tag === ViewTransitionComponent && + stopAtNestedViewTransitions + ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + // If this inner boundary resized we need to bubble that information up. + parentViewTransition.flags |= child.flags & AffectedParentLayout; + } else { + if ( measureViewTransitionHostInstancesRecursive( parentViewTransition, child.child, @@ -717,117 +826,14 @@ function measureViewTransitionHostInstancesRecursive( className, previousMeasurements, stopAtNestedViewTransitions, - ); - } - child = child.sibling; - } - return true; - } - let inViewport = false; - while (child !== null) { - if (child.tag === HostComponent) { - const instance: Instance = child.stateNode; - if ( - previousMeasurements !== null && - viewTransitionHostInstanceIdx < previousMeasurements.length - ) { - // The previous measurement of the Instance in this location within the ViewTransition. - // Note that this might not be the same exact Instance if the Instances within the - // ViewTransition changed. - const previousMeasurement = - previousMeasurements[viewTransitionHostInstanceIdx]; - const nextMeasurement = measureInstance(instance); - if ( - wasInstanceInViewport(previousMeasurement) || - wasInstanceInViewport(nextMeasurement) - ) { - // If either the old or new state was within the viewport we have to animate this. - // But if it turns out that none of them were we'll be able to skip it. - inViewport = true; - } - if ( - (parentViewTransition.flags & Update) === NoFlags && - hasInstanceChanged(previousMeasurement, nextMeasurement) - ) { - parentViewTransition.flags |= Update; - } - if ( - hasInstanceAffectedParent(previousMeasurement, nextMeasurement) - ) { - // If this instance size within its parent has changed it might have caused the - // parent to relayout which needs a cross fade. - parentViewTransition.flags |= AffectedParentLayout; - } - } else { - // If there was an insertion of extra nodes, we have to assume they affected the parent. - // It should have already been marked as an Update due to the mutation. - parentViewTransition.flags |= AffectedParentLayout; - } - if ((parentViewTransition.flags & Update) !== NoFlags) { - // We might update this node so we need to apply its new name for the new state. - // Additionally in the ApplyGesture case we also need to do this because the clone - // will have the name but this one won't. - applyViewTransitionName( - instance, - viewTransitionHostInstanceIdx === 0 - ? newName - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - newName + '_' + viewTransitionHostInstanceIdx, - className, - ); - } - if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) { - // It turns out that we had no other deeper mutations, the child transitions didn't - // affect the parent layout and this instance hasn't changed size. So we can skip - // animating it. However, in the current model this only works if the parent also - // doesn't animate. So we have to queue these and wait until we complete the parent - // to cancel them. - if (viewTransitionCancelableChildren === null) { - viewTransitionCancelableChildren = []; - } - viewTransitionCancelableChildren.push( - instance, - viewTransitionHostInstanceIdx === 0 - ? oldName - : // If we have multiple Host Instances below, we add a suffix to the name to give - // each one a unique name. - oldName + '_' + viewTransitionHostInstanceIdx, - child.memoizedProps, - ); - } - viewTransitionHostInstanceIdx++; - } else if ( - child.tag === OffscreenComponent && - child.memoizedState !== null - ) { - // Skip any hidden subtrees. They were or are effectively not there. - } else if ( - child.tag === ViewTransitionComponent && - stopAtNestedViewTransitions + ) ) { - // Skip any nested view transitions for updates since in that case the - // inner most one is the one that handles the update. - // If this inner boundary resized we need to bubble that information up. - parentViewTransition.flags |= child.flags & AffectedParentLayout; - } else { - if ( - measureViewTransitionHostInstancesRecursive( - parentViewTransition, - child.child, - newName, - oldName, - className, - previousMeasurements, - stopAtNestedViewTransitions, - ) - ) { - inViewport = true; - } + inViewport = true; } - child = child.sibling; } - return inViewport; + child = child.sibling; + } + return inViewport; } export function measureUpdateViewTransition( From 2c74b71c717e3a58323819609776e59ab8658784 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 09:28:48 -0400 Subject: [PATCH 07/30] Add TODO and comments for persistence mode measure logic --- .../src/ReactFiberCommitViewTransitions.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js index 8b2dd192dd9f..760270010dbc 100644 --- a/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js +++ b/packages/react-reconciler/src/ReactFiberCommitViewTransitions.js @@ -693,6 +693,15 @@ function measureViewTransitionHostInstancesRecursive( while (child !== null) { if (child.tag === HostComponent) { const instance: Instance = child.stateNode; + if ( + previousMeasurements == null || + viewTransitionHostInstanceIdx >= previousMeasurements.length + ) { + // If there was an insertion of extra nodes, we have to assume they affected the parent. + // It should have already been marked as an Update due to the mutation. + parentViewTransition.flags |= AffectedParentLayout; + } + // TODO: check if instance is out of viewport if ((parentViewTransition.flags & Update) !== NoFlags) { applyViewTransitionName( instance, @@ -702,6 +711,7 @@ function measureViewTransitionHostInstancesRecursive( className, ); } + // TODO: cancel transition by pushing into viewTransitionCancelableChildren viewTransitionHostInstanceIdx++; } else if ( child.tag === OffscreenComponent && @@ -712,6 +722,9 @@ function measureViewTransitionHostInstancesRecursive( child.tag === ViewTransitionComponent && stopAtNestedViewTransitions ) { + // Skip any nested view transitions for updates since in that case the + // inner most one is the one that handles the update. + // If this inner boundary resized we need to bubble that information up. parentViewTransition.flags |= child.flags & AffectedParentLayout; } else { measureViewTransitionHostInstancesRecursive( From a0ed77daa139e96698c73b9b7803d4c227bddf7d Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 13:09:56 -0400 Subject: [PATCH 08/30] Make enableViewTransitionForPersistenceMode a dynamic feature flag --- packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js | 1 + packages/shared/forks/ReactFeatureFlags.native-fb.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js index 40bb6f53c743..36774ad94d9f 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js @@ -26,3 +26,4 @@ export const enableFragmentRefsScrollIntoView = __VARIANT__; export const enableFragmentRefsInstanceHandles = __VARIANT__; export const enableEffectEventMutationPhase = __VARIANT__; export const enableFragmentRefsTextNodes = __VARIANT__; +export const enableViewTransitionForPersistenceMode = __VARIANT__; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index db97d4b19b1c..0e43e34d0009 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -28,6 +28,7 @@ export const { enableFragmentRefsScrollIntoView, enableFragmentRefsInstanceHandles, enableFragmentRefsTextNodes, + enableViewTransitionForPersistenceMode, } = dynamicFlags; // The rest of the flags are static for better dead code elimination. @@ -69,7 +70,6 @@ export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; export const enableViewTransition: boolean = true; -export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; export const enableSuspenseyImages: boolean = false; From 33b503de507a2dadb86961a6385e840ac6023c9b Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 14:13:56 -0500 Subject: [PATCH 09/30] create supportsViewTransition and ReactFiberConfigWithNoViewTransition out of ReactFiberConfigWithNoMutation --- packages/react-art/src/ReactFiberConfigART.js | 87 +--------- .../src/client/ReactFiberConfigDOM.js | 1 + .../src/ReactFiberConfigNative.js | 150 +----------------- .../src/ReactFiberCommitWork.js | 6 +- .../src/ReactFiberConfigWithNoMutation.js | 20 --- .../ReactFiberConfigWithNoViewTransition.js | 42 +++++ .../src/forks/ReactFiberConfig.custom.js | 1 + .../src/ReactFiberConfigTestHost.js | 146 +---------------- 8 files changed, 52 insertions(+), 401 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 50873af6da04..1ed9b04fa855 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -249,6 +249,7 @@ function applyTextProps(instance, props, prevProps = {}) { } } +export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; @@ -484,92 +485,6 @@ export function unhideTextInstance(textInstance, text): void { // Noop } -export function applyViewTransitionName(instance, name, className) { - // Noop -} - -export function restoreViewTransitionName(instance, props) { - // Noop -} - -export function cancelViewTransitionName(instance, name, props) { - // Noop -} - -export function cancelRootViewTransitionName(rootContainer) { - // Noop -} - -export function restoreRootViewTransitionName(rootContainer) { - // Noop -} - -export function cloneRootViewTransitionContainer(rootContainer) { - throw new Error('Not implemented.'); -} - -export function removeRootViewTransitionClone(rootContainer, clone) { - throw new Error('Not implemented.'); -} - -export type InstanceMeasurement = null; - -export function measureInstance(instance) { - return null; -} - -export function measureClonedInstance(instance) { - return null; -} - -export function wasInstanceInViewport(measurement): boolean { - return true; -} - -export function hasInstanceChanged(oldMeasurement, newMeasurement): boolean { - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement, - newMeasurement, -): boolean { - return false; -} - -export function startViewTransition() { - return null; -} - -export type RunningViewTransition = null; - -export function startGestureTransition() { - return null; -} - -export function stopViewTransition(transition: RunningViewTransition) {} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -) { - callback(); -} - -export type ViewTransitionInstance = null | {name: string, ...}; - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return null; -} - -export type GestureTimeline = null; - -export function getCurrentGestureOffset(provider: GestureTimeline): number { - throw new Error('startGestureTransition is not yet supported in react-art.'); -} - export function clearContainer(container) { // TODO Implement this } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 727ec694bbf7..41584e0840b8 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -877,6 +877,7 @@ function handleErrorInNextTick(error: any) { // ------------------- export const supportsMutation = true; +export const supportsViewTransition = true; export function commitMount( domElement: Instance, diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index 404ae7a54a85..b5e086493a01 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -8,7 +8,6 @@ */ import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType'; // Modules provided by RN: import { @@ -35,8 +34,6 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; -import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; - import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; @@ -112,6 +109,7 @@ function recursivelyUncacheFiberNode(node: Instance | TextInstance) { } } +export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; @@ -591,152 +589,6 @@ export function unhideInstance(instance: Instance, props: Props): void { ); } -export function applyViewTransitionName( - instance: Instance, - name: string, - className: ?string, -): void { - // Not yet implemented -} - -export function restoreViewTransitionName( - instance: Instance, - props: Props, -): void { - // Not yet implemented -} - -export function cancelViewTransitionName( - instance: Instance, - name: string, - props: Props, -): void { - // Not yet implemented -} - -export function cancelRootViewTransitionName(rootContainer: Container): void { - // Not yet implemented -} - -export function restoreRootViewTransitionName(rootContainer: Container): void { - // Not yet implemented -} - -export function cloneRootViewTransitionContainer( - rootContainer: Container, -): Instance { - throw new Error('Not implemented.'); -} - -export function removeRootViewTransitionClone( - rootContainer: Container, - clone: Instance, -): void { - throw new Error('Not implemented.'); -} - -export type InstanceMeasurement = null; - -export function measureInstance(instance: Instance): InstanceMeasurement { - // This heuristic is better implemented at the native layer. - return null; -} - -export function measureClonedInstance(instance: Instance): InstanceMeasurement { - return null; -} - -export function wasInstanceInViewport( - measurement: InstanceMeasurement, -): boolean { - return true; -} - -export function hasInstanceChanged( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function startViewTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - layoutCallback: () => void, - afterMutationCallback: () => void, - spawnedWorkCallback: () => void, - passiveCallback: () => mixed, - errorCallback: mixed => void, - blockedCallback: string => void, // Profiling-only - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - layoutCallback(); - // Skip afterMutationCallback(). We don't need it since we're not animating. - spawnedWorkCallback(); - if (enableProfilerTimer) { - finishedAnimation(); - } - // Skip passiveCallback(). Spawned work will schedule a task. - return null; -} - -export type RunningViewTransition = null; - -export function startGestureTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - timeline: GestureTimeline, - rangeStart: number, - rangeEnd: number, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - animateCallback: () => void, - errorCallback: mixed => void, - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - animateCallback(); - if (enableProfilerTimer) { - finishedAnimation(); - } - return null; -} - -export function stopViewTransition(transition: RunningViewTransition) {} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -) { - callback(); -} - -export type ViewTransitionInstance = null | {name: string, ...}; - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return null; -} - -export type GestureTimeline = null; - -export function getCurrentGestureOffset(provider: GestureTimeline): number { - throw new Error( - 'startGestureTransition is not yet supported in React Native.', - ); -} - export function clearContainer(container: Container): void { // TODO Implement this for React Native // UIManager does not expose a "remove all" type method. diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 322c858bb9c0..934189b67b12 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -158,6 +158,7 @@ import { supportsHydration, supportsResources, supportsSingletons, + supportsViewTransition, clearSuspenseBoundary, clearSuspenseBoundaryFromContainer, createContainerChildSet, @@ -3712,7 +3713,10 @@ function commitPassiveMountOnFiber( } if (isViewTransitionEligible) { - if (supportsMutation && rootViewTransitionNameCanceled) { + if ( + supportsViewTransition && + rootViewTransitionNameCanceled + ) { restoreRootViewTransitionName(finishedRoot.containerInfo); } } diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js index 79cf3990a728..f243805214d7 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js @@ -37,25 +37,5 @@ export const hideTextInstance = shim; export const unhideInstance = shim; export const unhideTextInstance = shim; export const clearContainer = shim; -export const applyViewTransitionName = shim; -export const restoreViewTransitionName = shim; -export const cancelViewTransitionName = shim; -export const cancelRootViewTransitionName = shim; -export const restoreRootViewTransitionName = shim; -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; -export const startViewTransition = shim; -export type RunningViewTransition = null; -export const startGestureTransition = shim; -export const stopViewTransition = shim; -export const addViewTransitionFinishedListener = shim; -export type ViewTransitionInstance = null | {name: string, ...}; -export const createViewTransitionInstance = shim; export type GestureTimeline = any; export const getCurrentGestureOffset = shim; diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js new file mode 100644 index 000000000000..471d969a5d8a --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Renderers that don't support view transitions +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'The current renderer does not support view transitions. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// View Transitions (when unsupported) +export const supportsViewTransition = false; +export const applyViewTransitionName = shim; +export const restoreViewTransitionName = shim; +export const cancelViewTransitionName = shim; +export const cancelRootViewTransitionName = shim; +export const restoreRootViewTransitionName = shim; +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; +export const startViewTransition = shim; +export type RunningViewTransition = null; +export const startGestureTransition = shim; +export const stopViewTransition = shim; +export const addViewTransitionFinishedListener = shim; +export type ViewTransitionInstance = null | {name: string, ...}; +export const createViewTransitionInstance = shim; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 1785fa9aaec9..4a2d87e8c40c 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -73,6 +73,7 @@ export const warnsIfNotActing = $$$config.warnsIfNotActing; export const supportsMutation = $$$config.supportsMutation; export const supportsPersistence = $$$config.supportsPersistence; export const supportsHydration = $$$config.supportsHydration; +export const supportsViewTransition = $$$config.supportsViewTransition; export const getInstanceFromNode = $$$config.getInstanceFromNode; export const beforeActiveInstanceBlur = $$$config.beforeActiveInstanceBlur; export const afterActiveInstanceBlur = $$$config.afterActiveInstanceBlur; diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 6b04a36d297a..417745828d60 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -8,8 +8,6 @@ */ import type {ReactContext} from 'shared/ReactTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType'; - import isArray from 'shared/isArray'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import { @@ -17,7 +15,6 @@ import { NoEventPriority, type EventPriority, } from 'react-reconciler/src/ReactEventPriorities'; -import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-test-renderer'; @@ -56,6 +53,7 @@ export type EventResponder = any; export type RendererInspectionConfig = $ReadOnly<{}>; export type TransitionStatus = mixed; +export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; @@ -332,148 +330,6 @@ export function unhideTextInstance( textInstance.isHidden = false; } -export function applyViewTransitionName( - instance: Instance, - name: string, - className: ?string, -): void { - // Noop -} - -export function restoreViewTransitionName( - instance: Instance, - props: Props, -): void { - // Noop -} - -export function cancelViewTransitionName( - instance: Instance, - name: string, - props: Props, -): void { - // Noop -} - -export function cancelRootViewTransitionName(rootContainer: Container): void { - // Noop -} - -export function restoreRootViewTransitionName(rootContainer: Container): void { - // Noop -} - -export function cloneRootViewTransitionContainer( - rootContainer: Container, -): Instance { - return { - type: 'ROOT', - props: {}, - isHidden: false, - children: [], - internalInstanceHandle: null, - rootContainerInstance: rootContainer, - tag: 'INSTANCE', - }; -} - -export function removeRootViewTransitionClone( - rootContainer: Container, - clone: Instance, -): void { - // Noop since it was never inserted anywhere. -} - -export type InstanceMeasurement = null; - -export function measureInstance(instance: Instance): InstanceMeasurement { - return null; -} - -export function measureClonedInstance(instance: Instance): InstanceMeasurement { - return null; -} - -export function wasInstanceInViewport( - measurement: InstanceMeasurement, -): boolean { - return true; -} - -export function hasInstanceChanged( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - return false; -} - -export function startViewTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - layoutCallback: () => void, - afterMutationCallback: () => void, - spawnedWorkCallback: () => void, - passiveCallback: () => mixed, - errorCallback: mixed => void, - blockedCallback: string => void, // Profiling-only - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - layoutCallback(); - // Skip afterMutationCallback(). We don't need it since we're not animating. - spawnedWorkCallback(); - // Skip passiveCallback(). Spawned work will schedule a task. - return null; -} - -export type RunningViewTransition = null; - -export function startGestureTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - timeline: GestureTimeline, - rangeStart: number, - rangeEnd: number, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - animateCallback: () => void, - errorCallback: mixed => void, - finishedAnimation: () => void, // Profiling-only -): null | RunningViewTransition { - mutationCallback(); - animateCallback(); - if (enableProfilerTimer) { - finishedAnimation(); - } - return null; -} - -export function stopViewTransition(transition: RunningViewTransition) {} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -) { - callback(); -} - -export type ViewTransitionInstance = null | {name: string, ...}; - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return null; -} - export type FragmentInstanceType = null; export function createFragmentInstance( From 1f1ecf580b253e034c765b9d081cc299647cc2ff Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 14:13:56 -0500 Subject: [PATCH 10/30] create supportsViewTransition and ReactFiberConfigWithNoViewTransition out of ReactFiberConfigWithNoMutation --- packages/react-art/src/ReactFiberConfigART.js | 1 + packages/react-native-renderer/src/ReactFiberConfigNative.js | 1 + packages/react-test-renderer/src/ReactFiberConfigTestHost.js | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 1ed9b04fa855..38628113b525 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -414,6 +414,7 @@ export const isPrimaryRenderer = false; export const warnsIfNotActing = false; export const supportsMutation = true; +export const supportsViewTransition = false; export function appendChild(parentInstance, child) { if (child.parentNode === parentInstance) { diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index b5e086493a01..e76635ccf759 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -374,6 +374,7 @@ export function shouldAttemptEagerTransition(): boolean { // ------------------- export const supportsMutation = true; +export const supportsViewTransition = false; export function appendChild( parentInstance: Instance, diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 417745828d60..e750f7a2b0a7 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -274,6 +274,7 @@ export const noTimeout: -1 = -1; // ------------------- export const supportsMutation = true; +export const supportsViewTransition = false; export function commitUpdate( instance: Instance, From 5d9cf1451cec557530f93be0dfc7d12dce3eb471 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Feb 2026 18:13:14 -0500 Subject: [PATCH 11/30] enableViewTransition for RN Summary: - turn on enableViewTransition feature - stub shim - run some mutation config fn at persistence mode too --- .../src/ReactFiberConfigFabric.js | 226 +++++++++++++++++- 1 file changed, 220 insertions(+), 6 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 18c4eaddd9ea..7831299ef1f7 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -12,6 +12,7 @@ import type { TouchedViewDataAtPoint, ViewConfig, } from './ReactNativeTypes'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; import {dispatchEvent} from './ReactFabricEventEmitter'; import { NoEventPriority, @@ -75,6 +76,12 @@ import {passChildrenWhenCloningPersistedNodes} from 'shared/ReactFeatureFlags'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; + export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-native-renderer'; export const extraDevToolsConfig = { @@ -160,12 +167,219 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } -export * from 'react-reconciler/src/ReactFiberConfigWithNoMutation'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; +// ------------------- +// Mutation +// ------------------- + +function shim(...args: any): empty { + throw new Error( + 'The current renderer does not support mutation. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +export const supportsMutation = false; + +export const cloneMutableInstance = shim; +export const cloneMutableTextInstance = shim; +export const appendChild = shim; +export const appendChildToContainer = shim; +export const commitTextUpdate = shim; + +export function commitMount( + instance: Instance, + type: string, + newProps: Props, + internalInstanceHandle: Object, +): void { + console.log('[shim] commitMount'); +} + +export const commitUpdate = shim; +export const insertBefore = shim; +export const insertInContainerBefore = shim; +export const removeChild = shim; +export const removeChildFromContainer = shim; +export const resetTextContent = shim; +export const hideInstance = shim; +export const hideTextInstance = shim; +export const unhideInstance = shim; +export const unhideTextInstance = shim; +export const clearContainer = shim; + +export type InstanceMeasurement = { + rect: {x: number, y: number, width: number, height: number}, + abs: boolean, + clip: boolean, + view: boolean, +}; + +export type RunningViewTransition = null; + +export type ViewTransitionInstance = null | { + name: string, + ... +}; + +export type GestureTimeline = any; + +export function restoreViewTransitionName( + instance: Instance, + props: Props, +): void { + console.log('[shim] restoreViewTransitionName ', instance.canonical.nativeTag); +} + +export function cancelViewTransitionName( + instance: Instance, + oldName: string, + props: Props, +): void { + console.log('[shim] cancelViewTransitionName ', oldName, instance.canonical.nativeTag); +} + +export function cancelRootViewTransitionName(rootContainer: Container): void { + console.log('[shim] cancelRootViewTransitionName'); +} + +export function restoreRootViewTransitionName(rootContainer: Container): void { + console.log('[shim] restoreRootViewTransitionName'); +} + +export function cloneRootViewTransitionContainer( + rootContainer: Container, +): Instance { + console.log('[shim] cloneRootViewTransitionContainer'); +} + +export function removeRootViewTransitionClone( + rootContainer: Container, + clone: Instance, +): void { + console.log('[shim] removeRootViewTransitionClone'); +} + +export function measureInstance(instance: Instance): InstanceMeasurement { + console.log('[shim] measureInstance ', instance.canonical.nativeTag); + return {rect: {x: 0, y: 0, width: 0, height: 0}, abs: false, clip: false, view: true}; +} + +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + console.log('[shim] measureClonedInstance ', instance.canonical.nativeTag); + return {rect: {x: 0, y: 0, width: 0, height: 0}, abs: false, clip: false, view: true}; +} + +export function wasInstanceInViewport( + measurement: InstanceMeasurement, +): boolean { + console.log('[shim] wasInstanceInViewport'); + return measurement.view; +} + +export function hasInstanceChanged( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + console.log('[shim] hasInstanceChanged'); + return false; +} + +export function hasInstanceAffectedParent( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + console.log('[shim] hasInstanceAffectedParent'); + return false; +} + +export function startGestureTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + timeline: GestureTimeline, + rangeStart: number, + rangeEnd: number, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + animateCallback: () => void, + errorCallback: (error: mixed) => void, + finishedAnimation: () => void, +): RunningViewTransition { + console.log('[shim] startGestureTransition'); + return null; +} + +export function stopViewTransition(transition: RunningViewTransition): void { + console.log('[shim] stopViewTransition'); +} + +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +): void { + console.log('[shim] addViewTransitionFinishedListener'); + callback(); +} + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + console.log('[shim] createViewTransitionInstance', name); + return {name}; +} + +export function getCurrentGestureOffset(timeline: GestureTimeline): number { + console.log('[shim] getCurrentGestureOffset'); + return 0; +} + +export function applyViewTransitionName( + instance: Instance, + name: string, + className: ?string, +): void { + // add view-transition-name to things that might animate for browser + console.log('[shim] applyViewTransitionName', name, className, instance.canonical.nativeTag); +} + +export function startViewTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, + passiveCallback: () => mixed, + errorCallback: (error: mixed) => void, + blockedCallback: (name: string) => void, + finishedAnimation: () => void, +): RunningViewTransition { + console.log('[shim] startViewTransition transitionTypes', JSON.stringify(transitionTypes ?? [])); + + // mutation phase + console.log('[shim] startViewTransition mutations start'); + mutationCallback(); + layoutCallback(); // run layout effects + afterMutationCallback(); + console.log('[shim] startViewTransition mutations finish'); + + // browser creates pseudo elements + console.log('[shim] startViewTransition pseudo element captured'); + + // transition ready + console.log('[shim] startViewTransition transition ready'); + spawnedWorkCallback(); + + console.log('[shim] startViewTransition finishedAnimation'); + // finishedAnimation(); + + // transition ends + console.log('[shim] startViewTransition transition ends'); + passiveCallback(); + + return null; +} export function appendInitialChild( parentInstance: Instance, From 48dede40b5f7f23fe5829e3254e99e60bf5fc573 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 6 Feb 2026 16:07:31 -0500 Subject: [PATCH 12/30] invoke FabricUIManager methods --- .../src/ReactFiberConfigFabric.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 7831299ef1f7..c0c4e224be76 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -61,6 +61,10 @@ const { unstable_ContinuousEventPriority: FabricContinuousPriority, unstable_IdleEventPriority: FabricIdlePriority, unstable_getCurrentEventPriority: fabricGetCurrentEventPriority, + measureInstance: fabricMeasureInstance, + applyViewTransitionName: fabricApplyViewTransitionName, + executeViewTransition: fabricExecuteViewTransition, + startViewTransition: fabricStartViewTransition, } = nativeFabricUIManager; import {getClosestInstanceFromNode} from './ReactFabricComponentTree'; @@ -262,7 +266,18 @@ export function removeRootViewTransitionClone( export function measureInstance(instance: Instance): InstanceMeasurement { console.log('[shim] measureInstance ', instance.canonical.nativeTag); - return {rect: {x: 0, y: 0, width: 0, height: 0}, abs: false, clip: false, view: true}; + var measurement = fabricMeasureInstance(instance.node); + return { + rect: { + x: measurement.x, + y: measurement.y, + width: measurement.width, + height: measurement.height + }, + abs: false, + clip: false, + view: true + }; } export function measureClonedInstance(instance: Instance): InstanceMeasurement { @@ -340,6 +355,7 @@ export function applyViewTransitionName( ): void { // add view-transition-name to things that might animate for browser console.log('[shim] applyViewTransitionName', name, className, instance.canonical.nativeTag); + fabricApplyViewTransitionName(instance.node, name, className); } export function startViewTransition( @@ -356,6 +372,7 @@ export function startViewTransition( finishedAnimation: () => void, ): RunningViewTransition { console.log('[shim] startViewTransition transitionTypes', JSON.stringify(transitionTypes ?? [])); + fabricStartViewTransition(); // mutation phase console.log('[shim] startViewTransition mutations start'); @@ -370,6 +387,7 @@ export function startViewTransition( // transition ready console.log('[shim] startViewTransition transition ready'); spawnedWorkCallback(); + fabricExecuteViewTransition(); console.log('[shim] startViewTransition finishedAnimation'); // finishedAnimation(); From a7a00e3b818b2d18175b5b387eb5e6bba84b49ab Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 6 Feb 2026 17:42:16 -0500 Subject: [PATCH 13/30] Rename fabricStartViewTransition to fabricViewTransitionStarted Better reflects that this signals the transition has started rather than initiating it. --- .../src/ReactFiberConfigFabric.js | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index c0c4e224be76..9e03d11e9007 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -63,7 +63,6 @@ const { unstable_getCurrentEventPriority: fabricGetCurrentEventPriority, measureInstance: fabricMeasureInstance, applyViewTransitionName: fabricApplyViewTransitionName, - executeViewTransition: fabricExecuteViewTransition, startViewTransition: fabricStartViewTransition, } = nativeFabricUIManager; @@ -371,30 +370,33 @@ export function startViewTransition( blockedCallback: (name: string) => void, finishedAnimation: () => void, ): RunningViewTransition { - console.log('[shim] startViewTransition transitionTypes', JSON.stringify(transitionTypes ?? [])); - fabricStartViewTransition(); - - // mutation phase - console.log('[shim] startViewTransition mutations start'); - mutationCallback(); - layoutCallback(); // run layout effects - afterMutationCallback(); - console.log('[shim] startViewTransition mutations finish'); - - // browser creates pseudo elements - console.log('[shim] startViewTransition pseudo element captured'); - - // transition ready - console.log('[shim] startViewTransition transition ready'); - spawnedWorkCallback(); - fabricExecuteViewTransition(); - - console.log('[shim] startViewTransition finishedAnimation'); - // finishedAnimation(); + console.log( + "[shim] startViewTransition transitionTypes", + JSON.stringify(null != transitionTypes ? transitionTypes : []) + ); - // transition ends - console.log('[shim] startViewTransition transition ends'); - passiveCallback(); + fabricStartViewTransition( + // mutation + ()=>{ + console.log("[shim] startViewTransition mutations start"); + // completeRoot should run here + mutationCallback(); + layoutCallback(); + afterMutationCallback(); + console.log("[shim] startViewTransition mutations finish"); + }, + // onReady + ()=>{ + console.log("[shim] startViewTransition pseudo element captured"); + console.log("[shim] startViewTransition transition ready"); + spawnedWorkCallback(); + }, + // onComplete + ()=>{ + console.log("[shim] startViewTransition finishedAnimation"); + console.log("[shim] startViewTransition transition ends"); + passiveCallback(); + }); return null; } @@ -827,6 +829,7 @@ export function replaceContainerChildren( container: Container, newChildren: ChildSet, ): void { + console.log('completeRoot'); completeRoot(container.containerTag, newChildren); } From 55669e6bc089a9618cef0f732285b6010842e4ee Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 11:59:03 -0500 Subject: [PATCH 14/30] restore/cancelViewTransitionName; fallback behavior if fabric doesnt enable startViewTransition --- .../src/ReactFiberConfigFabric.js | 77 +++++++++---------- 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 9e03d11e9007..d8d2b385a3a1 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -64,6 +64,8 @@ const { measureInstance: fabricMeasureInstance, applyViewTransitionName: fabricApplyViewTransitionName, startViewTransition: fabricStartViewTransition, + restoreViewTransitionName: fabricRestoreViewTransitionName, + cancelViewTransitionName: fabricCancelViewTransitionName, } = nativeFabricUIManager; import {getClosestInstanceFromNode} from './ReactFabricComponentTree'; @@ -79,12 +81,6 @@ import {passChildrenWhenCloningPersistedNodes} from 'shared/ReactFeatureFlags'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; -export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; - export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-native-renderer'; export const extraDevToolsConfig = { @@ -170,8 +166,14 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } +export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; +export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; + // ------------------- -// Mutation +// ViewTransition // ------------------- function shim(...args: any): empty { @@ -195,9 +197,7 @@ export function commitMount( type: string, newProps: Props, internalInstanceHandle: Object, -): void { - console.log('[shim] commitMount'); -} +): void {} export const commitUpdate = shim; export const insertBefore = shim; @@ -231,41 +231,41 @@ export function restoreViewTransitionName( instance: Instance, props: Props, ): void { - console.log('[shim] restoreViewTransitionName ', instance.canonical.nativeTag); + fabricRestoreViewTransitionName(instance.node); } +// Cancel the old and new snapshots of viewTransitionName export function cancelViewTransitionName( instance: Instance, oldName: string, props: Props, ): void { - console.log('[shim] cancelViewTransitionName ', oldName, instance.canonical.nativeTag); + fabricCancelViewTransitionName(instance.node, oldName); } export function cancelRootViewTransitionName(rootContainer: Container): void { - console.log('[shim] cancelRootViewTransitionName'); + } export function restoreRootViewTransitionName(rootContainer: Container): void { - console.log('[shim] restoreRootViewTransitionName'); + } export function cloneRootViewTransitionContainer( rootContainer: Container, ): Instance { - console.log('[shim] cloneRootViewTransitionContainer'); } export function removeRootViewTransitionClone( rootContainer: Container, clone: Instance, ): void { - console.log('[shim] removeRootViewTransitionClone'); + } export function measureInstance(instance: Instance): InstanceMeasurement { - console.log('[shim] measureInstance ', instance.canonical.nativeTag); - var measurement = fabricMeasureInstance(instance.node); + + const measurement = fabricMeasureInstance(instance.node); return { rect: { x: measurement.x, @@ -280,14 +280,12 @@ export function measureInstance(instance: Instance): InstanceMeasurement { } export function measureClonedInstance(instance: Instance): InstanceMeasurement { - console.log('[shim] measureClonedInstance ', instance.canonical.nativeTag); return {rect: {x: 0, y: 0, width: 0, height: 0}, abs: false, clip: false, view: true}; } export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { - console.log('[shim] wasInstanceInViewport'); return measurement.view; } @@ -295,7 +293,6 @@ export function hasInstanceChanged( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { - console.log('[shim] hasInstanceChanged'); return false; } @@ -303,7 +300,6 @@ export function hasInstanceAffectedParent( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { - console.log('[shim] hasInstanceAffectedParent'); return false; } @@ -319,31 +315,26 @@ export function startGestureTransition( errorCallback: (error: mixed) => void, finishedAnimation: () => void, ): RunningViewTransition { - console.log('[shim] startGestureTransition'); return null; } export function stopViewTransition(transition: RunningViewTransition): void { - console.log('[shim] stopViewTransition'); } export function addViewTransitionFinishedListener( transition: RunningViewTransition, callback: () => void, ): void { - console.log('[shim] addViewTransitionFinishedListener'); callback(); } export function createViewTransitionInstance( name: string, ): ViewTransitionInstance { - console.log('[shim] createViewTransitionInstance', name); return {name}; } export function getCurrentGestureOffset(timeline: GestureTimeline): number { - console.log('[shim] getCurrentGestureOffset'); return 0; } @@ -353,7 +344,6 @@ export function applyViewTransitionName( className: ?string, ): void { // add view-transition-name to things that might animate for browser - console.log('[shim] applyViewTransitionName', name, className, instance.canonical.nativeTag); fabricApplyViewTransitionName(instance.node, name, className); } @@ -370,34 +360,38 @@ export function startViewTransition( blockedCallback: (name: string) => void, finishedAnimation: () => void, ): RunningViewTransition { - console.log( - "[shim] startViewTransition transitionTypes", - JSON.stringify(null != transitionTypes ? transitionTypes : []) - ); - fabricStartViewTransition( + const startedTransition = fabricStartViewTransition( // mutation ()=>{ - console.log("[shim] startViewTransition mutations start"); - // completeRoot should run here - mutationCallback(); + mutationCallback(); // completeRoot should run here layoutCallback(); afterMutationCallback(); - console.log("[shim] startViewTransition mutations finish"); }, // onReady ()=>{ - console.log("[shim] startViewTransition pseudo element captured"); - console.log("[shim] startViewTransition transition ready"); spawnedWorkCallback(); }, // onComplete ()=>{ - console.log("[shim] startViewTransition finishedAnimation"); - console.log("[shim] startViewTransition transition ends"); passiveCallback(); }); + if (!startedTransition) { + if (__DEV__) { + console.warn( + "startViewTransition didn't kick off transition in Fabric, the ViewTransition ReactNativeFeatureFlag might not be enabled.", + ); + } + // Flush remaining work synchronously. + mutationCallback(); + layoutCallback(); + // Skip afterMutationCallback(). We don't need it since we're not animating. + spawnedWorkCallback(); + // Skip passiveCallback(). Spawned work will schedule a task. + return null; + } + return null; } @@ -829,7 +823,6 @@ export function replaceContainerChildren( container: Container, newChildren: ChildSet, ): void { - console.log('completeRoot'); completeRoot(container.containerTag, newChildren); } From ef247be65f105b6a690d03c39460e90990f3300b Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 12:06:37 -0500 Subject: [PATCH 15/30] fix flow --- .../src/ReactFiberConfigFabric.js | 35 +++++++++++++++++-- scripts/flow/react-native-host-hooks.js | 18 ++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index d8d2b385a3a1..fc8780f6888f 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -244,23 +244,34 @@ export function cancelViewTransitionName( } export function cancelRootViewTransitionName(rootContainer: Container): void { - + if (__DEV__) { + console.warn('cancelRootViewTransitionName is not implemented'); + } } export function restoreRootViewTransitionName(rootContainer: Container): void { - + if (__DEV__) { + console.warn('restoreRootViewTransitionName is not implemented'); + } } export function cloneRootViewTransitionContainer( rootContainer: Container, ): Instance { + if (__DEV__) { + console.warn('cloneRootViewTransitionContainer is not implemented'); + } + // $FlowFixMe[incompatible-return] Return empty stub + return null; } export function removeRootViewTransitionClone( rootContainer: Container, clone: Instance, ): void { - + if (__DEV__) { + console.warn('removeRootViewTransitionClone is not implemented'); + } } export function measureInstance(instance: Instance): InstanceMeasurement { @@ -280,6 +291,9 @@ export function measureInstance(instance: Instance): InstanceMeasurement { } export function measureClonedInstance(instance: Instance): InstanceMeasurement { + if (__DEV__) { + console.warn('measureClonedInstance is not implemented'); + } return {rect: {x: 0, y: 0, width: 0, height: 0}, abs: false, clip: false, view: true}; } @@ -293,6 +307,9 @@ export function hasInstanceChanged( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { + if (__DEV__) { + console.warn('hasInstanceChanged is not implemented'); + } return false; } @@ -300,6 +317,9 @@ export function hasInstanceAffectedParent( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { + if (__DEV__) { + console.warn('hasInstanceAffectedParent is not implemented'); + } return false; } @@ -315,10 +335,16 @@ export function startGestureTransition( errorCallback: (error: mixed) => void, finishedAnimation: () => void, ): RunningViewTransition { + if (__DEV__) { + console.warn('startGestureTransition is not implemented'); + } return null; } export function stopViewTransition(transition: RunningViewTransition): void { + if (__DEV__) { + console.warn('stopViewTransition is not implemented'); + } } export function addViewTransitionFinishedListener( @@ -335,6 +361,9 @@ export function createViewTransitionInstance( } export function getCurrentGestureOffset(timeline: GestureTimeline): number { + if (__DEV__) { + console.warn('getCurrentGestureOffset is not implemented'); + } return 0; } diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 227c78bca24a..db8bbd9efaee 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -301,5 +301,23 @@ declare const nativeFabricUIManager: { unstable_ContinuousEventPriority: number, unstable_IdleEventPriority: number, unstable_getCurrentEventPriority: () => number, + measureInstance: (node: Object) => { + x: number, + y: number, + width: number, + height: number, + }, + applyViewTransitionName: ( + node: Object, + name: string, + className: ?string, + ) => void, + startViewTransition: ( + mutationCallback: () => void, + onReady: () => void, + onComplete: () => void, + ) => boolean, + restoreViewTransitionName: (node: Object) => void, + cancelViewTransitionName: (node: Object, oldName: string) => void, ... }; From 6fb21be4183a1f35970f6fcf610d1159ce9a04cd Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 12:21:29 -0500 Subject: [PATCH 16/30] prettier --- .../src/ReactFiberConfigFabric.js | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index fc8780f6888f..24fc960b14d3 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -275,18 +275,17 @@ export function removeRootViewTransitionClone( } export function measureInstance(instance: Instance): InstanceMeasurement { - const measurement = fabricMeasureInstance(instance.node); return { rect: { x: measurement.x, y: measurement.y, width: measurement.width, - height: measurement.height + height: measurement.height, }, abs: false, clip: false, - view: true + view: true, }; } @@ -294,7 +293,12 @@ export function measureClonedInstance(instance: Instance): InstanceMeasurement { if (__DEV__) { console.warn('measureClonedInstance is not implemented'); } - return {rect: {x: 0, y: 0, width: 0, height: 0}, abs: false, clip: false, view: true}; + return { + rect: {x: 0, y: 0, width: 0, height: 0}, + abs: false, + clip: false, + view: true, + }; } export function wasInstanceInViewport( @@ -389,22 +393,22 @@ export function startViewTransition( blockedCallback: (name: string) => void, finishedAnimation: () => void, ): RunningViewTransition { - const startedTransition = fabricStartViewTransition( // mutation - ()=>{ - mutationCallback(); // completeRoot should run here - layoutCallback(); - afterMutationCallback(); - }, - // onReady - ()=>{ - spawnedWorkCallback(); - }, - // onComplete - ()=>{ - passiveCallback(); - }); + () => { + mutationCallback(); // completeRoot should run here + layoutCallback(); + afterMutationCallback(); + }, + // onReady + () => { + spawnedWorkCallback(); + }, + // onComplete + () => { + passiveCallback(); + }, + ); if (!startedTransition) { if (__DEV__) { @@ -412,7 +416,7 @@ export function startViewTransition( "startViewTransition didn't kick off transition in Fabric, the ViewTransition ReactNativeFeatureFlag might not be enabled.", ); } - // Flush remaining work synchronously. + // Flush remaining work synchronously. mutationCallback(); layoutCallback(); // Skip afterMutationCallback(). We don't need it since we're not animating. From 85fdaa00d8e2e1269d231a53bffa7e907fd226f8 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 13 Feb 2026 12:32:19 -0500 Subject: [PATCH 17/30] make startViewTransition async and return ready & finished promises --- .../src/ReactFiberConfigFabric.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 24fc960b14d3..c5f58af37772 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -218,7 +218,12 @@ export type InstanceMeasurement = { view: boolean, }; -export type RunningViewTransition = null; +export type RunningViewTransition = { + skipTransition(): void, + finished: Promise, + ready: Promise, + ... +}; export type ViewTransitionInstance = null | { name: string, @@ -392,8 +397,8 @@ export function startViewTransition( errorCallback: (error: mixed) => void, blockedCallback: (name: string) => void, finishedAnimation: () => void, -): RunningViewTransition { - const startedTransition = fabricStartViewTransition( +): null | RunningViewTransition { + const transition = fabricStartViewTransition( // mutation () => { mutationCallback(); // completeRoot should run here @@ -410,7 +415,7 @@ export function startViewTransition( }, ); - if (!startedTransition) { + if (transition == null) { if (__DEV__) { console.warn( "startViewTransition didn't kick off transition in Fabric, the ViewTransition ReactNativeFeatureFlag might not be enabled.", @@ -425,7 +430,7 @@ export function startViewTransition( return null; } - return null; + return transition; } export function appendInitialChild( From c9ba98df0de02fe247cce71c8a9ede279f5dcab8 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 25 Feb 2026 14:24:19 -0500 Subject: [PATCH 18/30] update startViewTransition to use ready/finished promise returned from fabric --- .../src/ReactFiberConfigFabric.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index c5f58af37772..c53420bdf98a 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -405,14 +405,6 @@ export function startViewTransition( layoutCallback(); afterMutationCallback(); }, - // onReady - () => { - spawnedWorkCallback(); - }, - // onComplete - () => { - passiveCallback(); - }, ); if (transition == null) { @@ -430,6 +422,14 @@ export function startViewTransition( return null; } + transition.ready.then(() => { + spawnedWorkCallback(); + }); + + transition.finished.finally(() => { + passiveCallback(); + }); + return transition; } From b3428f24ba1f243e31557a5d30e4c76e3def8609 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 14:13:21 -0500 Subject: [PATCH 19/30] move vt specific config functions to ReactFiberConfigFabricWithViewTransition.js --- .../src/ReactFiberConfigFabric.js | 233 +---------------- ...eactFiberConfigFabricWithViewTransition.js | 240 ++++++++++++++++++ 2 files changed, 244 insertions(+), 229 deletions(-) create mode 100644 packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index c53420bdf98a..5fe1961ab3ad 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -12,7 +12,6 @@ import type { TouchedViewDataAtPoint, ViewConfig, } from './ReactNativeTypes'; -import type {TransitionTypes} from 'react/src/ReactTransitionType'; import {dispatchEvent} from './ReactFabricEventEmitter'; import { NoEventPriority, @@ -61,11 +60,6 @@ const { unstable_ContinuousEventPriority: FabricContinuousPriority, unstable_IdleEventPriority: FabricIdlePriority, unstable_getCurrentEventPriority: fabricGetCurrentEventPriority, - measureInstance: fabricMeasureInstance, - applyViewTransitionName: fabricApplyViewTransitionName, - startViewTransition: fabricStartViewTransition, - restoreViewTransitionName: fabricRestoreViewTransitionName, - cancelViewTransitionName: fabricCancelViewTransitionName, } = nativeFabricUIManager; import {getClosestInstanceFromNode} from './ReactFabricComponentTree'; @@ -166,14 +160,17 @@ if (registerEventHandler) { registerEventHandler(dispatchEvent); } +export * from 'react-reconciler/src/ReactFiberConfigWithNoMutation'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; +export * from './ReactFiberConfigFabricWithViewTransition'; // ------------------- -// ViewTransition +// Mutation +// (not supported) // ------------------- function shim(...args: any): empty { @@ -211,228 +208,6 @@ export const unhideInstance = shim; export const unhideTextInstance = shim; export const clearContainer = shim; -export type InstanceMeasurement = { - rect: {x: number, y: number, width: number, height: number}, - abs: boolean, - clip: boolean, - view: boolean, -}; - -export type RunningViewTransition = { - skipTransition(): void, - finished: Promise, - ready: Promise, - ... -}; - -export type ViewTransitionInstance = null | { - name: string, - ... -}; - -export type GestureTimeline = any; - -export function restoreViewTransitionName( - instance: Instance, - props: Props, -): void { - fabricRestoreViewTransitionName(instance.node); -} - -// Cancel the old and new snapshots of viewTransitionName -export function cancelViewTransitionName( - instance: Instance, - oldName: string, - props: Props, -): void { - fabricCancelViewTransitionName(instance.node, oldName); -} - -export function cancelRootViewTransitionName(rootContainer: Container): void { - if (__DEV__) { - console.warn('cancelRootViewTransitionName is not implemented'); - } -} - -export function restoreRootViewTransitionName(rootContainer: Container): void { - if (__DEV__) { - console.warn('restoreRootViewTransitionName is not implemented'); - } -} - -export function cloneRootViewTransitionContainer( - rootContainer: Container, -): Instance { - if (__DEV__) { - console.warn('cloneRootViewTransitionContainer is not implemented'); - } - // $FlowFixMe[incompatible-return] Return empty stub - return null; -} - -export function removeRootViewTransitionClone( - rootContainer: Container, - clone: Instance, -): void { - if (__DEV__) { - console.warn('removeRootViewTransitionClone is not implemented'); - } -} - -export function measureInstance(instance: Instance): InstanceMeasurement { - const measurement = fabricMeasureInstance(instance.node); - return { - rect: { - x: measurement.x, - y: measurement.y, - width: measurement.width, - height: measurement.height, - }, - abs: false, - clip: false, - view: true, - }; -} - -export function measureClonedInstance(instance: Instance): InstanceMeasurement { - if (__DEV__) { - console.warn('measureClonedInstance is not implemented'); - } - return { - rect: {x: 0, y: 0, width: 0, height: 0}, - abs: false, - clip: false, - view: true, - }; -} - -export function wasInstanceInViewport( - measurement: InstanceMeasurement, -): boolean { - return measurement.view; -} - -export function hasInstanceChanged( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - if (__DEV__) { - console.warn('hasInstanceChanged is not implemented'); - } - return false; -} - -export function hasInstanceAffectedParent( - oldMeasurement: InstanceMeasurement, - newMeasurement: InstanceMeasurement, -): boolean { - if (__DEV__) { - console.warn('hasInstanceAffectedParent is not implemented'); - } - return false; -} - -export function startGestureTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - timeline: GestureTimeline, - rangeStart: number, - rangeEnd: number, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - animateCallback: () => void, - errorCallback: (error: mixed) => void, - finishedAnimation: () => void, -): RunningViewTransition { - if (__DEV__) { - console.warn('startGestureTransition is not implemented'); - } - return null; -} - -export function stopViewTransition(transition: RunningViewTransition): void { - if (__DEV__) { - console.warn('stopViewTransition is not implemented'); - } -} - -export function addViewTransitionFinishedListener( - transition: RunningViewTransition, - callback: () => void, -): void { - callback(); -} - -export function createViewTransitionInstance( - name: string, -): ViewTransitionInstance { - return {name}; -} - -export function getCurrentGestureOffset(timeline: GestureTimeline): number { - if (__DEV__) { - console.warn('getCurrentGestureOffset is not implemented'); - } - return 0; -} - -export function applyViewTransitionName( - instance: Instance, - name: string, - className: ?string, -): void { - // add view-transition-name to things that might animate for browser - fabricApplyViewTransitionName(instance.node, name, className); -} - -export function startViewTransition( - suspendedState: null | SuspendedState, - rootContainer: Container, - transitionTypes: null | TransitionTypes, - mutationCallback: () => void, - layoutCallback: () => void, - afterMutationCallback: () => void, - spawnedWorkCallback: () => void, - passiveCallback: () => mixed, - errorCallback: (error: mixed) => void, - blockedCallback: (name: string) => void, - finishedAnimation: () => void, -): null | RunningViewTransition { - const transition = fabricStartViewTransition( - // mutation - () => { - mutationCallback(); // completeRoot should run here - layoutCallback(); - afterMutationCallback(); - }, - ); - - if (transition == null) { - if (__DEV__) { - console.warn( - "startViewTransition didn't kick off transition in Fabric, the ViewTransition ReactNativeFeatureFlag might not be enabled.", - ); - } - // Flush remaining work synchronously. - mutationCallback(); - layoutCallback(); - // Skip afterMutationCallback(). We don't need it since we're not animating. - spawnedWorkCallback(); - // Skip passiveCallback(). Spawned work will schedule a task. - return null; - } - - transition.ready.then(() => { - spawnedWorkCallback(); - }); - - transition.finished.finally(() => { - passiveCallback(); - }); - - return transition; -} - export function appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js new file mode 100644 index 000000000000..0d8796927f2c --- /dev/null +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -0,0 +1,240 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {TransitionTypes} from 'react/src/ReactTransitionType'; +import type { + Instance, + Props, + Container, + SuspendedState, + GestureTimeline, +} from './ReactFiberConfigFabric'; + +const { + measureInstance: fabricMeasureInstance, + applyViewTransitionName: fabricApplyViewTransitionName, + startViewTransition: fabricStartViewTransition, + restoreViewTransitionName: fabricRestoreViewTransitionName, + cancelViewTransitionName: fabricCancelViewTransitionName, +} = nativeFabricUIManager; + +export const supportsViewTransition = true; + +export type InstanceMeasurement = { + rect: {x: number, y: number, width: number, height: number}, + abs: boolean, + clip: boolean, + view: boolean, +}; + +export type RunningViewTransition = { + skipTransition(): void, + finished: Promise, + ready: Promise, + ... +}; + +export type ViewTransitionInstance = null | { + name: string, + ... +}; + +export function restoreViewTransitionName( + instance: Instance, + props: Props, +): void { + fabricRestoreViewTransitionName(instance.node); +} + +// Cancel the old and new snapshots of viewTransitionName +export function cancelViewTransitionName( + instance: Instance, + oldName: string, + props: Props, +): void { + fabricCancelViewTransitionName(instance.node, oldName); +} + +export function cancelRootViewTransitionName(rootContainer: Container): void { + if (__DEV__) { + console.warn('cancelRootViewTransitionName is not implemented'); + } +} + +export function restoreRootViewTransitionName(rootContainer: Container): void { + if (__DEV__) { + console.warn('restoreRootViewTransitionName is not implemented'); + } +} + +export function cloneRootViewTransitionContainer( + rootContainer: Container, +): Instance { + if (__DEV__) { + console.warn('cloneRootViewTransitionContainer is not implemented'); + } + // $FlowFixMe[incompatible-return] Return empty stub + return null; +} + +export function removeRootViewTransitionClone( + rootContainer: Container, + clone: Instance, +): void { + if (__DEV__) { + console.warn('removeRootViewTransitionClone is not implemented'); + } +} + +export function measureInstance(instance: Instance): InstanceMeasurement { + const measurement = fabricMeasureInstance(instance.node); + return { + rect: { + x: measurement.x, + y: measurement.y, + width: measurement.width, + height: measurement.height, + }, + abs: false, + clip: false, + view: true, + }; +} + +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + if (__DEV__) { + console.warn('measureClonedInstance is not implemented'); + } + return { + rect: {x: 0, y: 0, width: 0, height: 0}, + abs: false, + clip: false, + view: true, + }; +} + +export function wasInstanceInViewport( + measurement: InstanceMeasurement, +): boolean { + return measurement.view; +} + +export function hasInstanceChanged( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + if (__DEV__) { + console.warn('hasInstanceChanged is not implemented'); + } + return false; +} + +export function hasInstanceAffectedParent( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + if (__DEV__) { + console.warn('hasInstanceAffectedParent is not implemented'); + } + return false; +} + +export function startGestureTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + timeline: GestureTimeline, + rangeStart: number, + rangeEnd: number, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + animateCallback: () => void, + errorCallback: (error: mixed) => void, + finishedAnimation: () => void, +): RunningViewTransition { + if (__DEV__) { + console.warn('startGestureTransition is not implemented'); + } + return null; +} + +export function stopViewTransition(transition: RunningViewTransition): void { + if (__DEV__) { + console.warn('stopViewTransition is not implemented'); + } +} + +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +): void { + callback(); +} + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return {name}; +} + +export function applyViewTransitionName( + instance: Instance, + name: string, + className: ?string, +): void { + // add view-transition-name to things that might animate for browser + fabricApplyViewTransitionName(instance.node, name, className); +} + +export function startViewTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, + passiveCallback: () => mixed, + errorCallback: (error: mixed) => void, + blockedCallback: (name: string) => void, + finishedAnimation: () => void, +): null | RunningViewTransition { + const transition = fabricStartViewTransition( + // mutation + () => { + mutationCallback(); // completeRoot should run here + layoutCallback(); + afterMutationCallback(); + }, + ); + + if (transition == null) { + if (__DEV__) { + console.warn( + "startViewTransition didn't kick off transition in Fabric, the ViewTransition ReactNativeFeatureFlag might not be enabled.", + ); + } + // Flush remaining work synchronously. + mutationCallback(); + layoutCallback(); + // Skip afterMutationCallback(). We don't need it since we're not animating. + spawnedWorkCallback(); + // Skip passiveCallback(). Spawned work will schedule a task. + return null; + } + + transition.ready.then(() => { + spawnedWorkCallback(); + }); + + transition.finished.finally(() => { + passiveCallback(); + }); + + return transition; +} From cdaf33e907867eb48558dc19e1520a5b5fc9b983 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 16:04:33 -0500 Subject: [PATCH 20/30] implement function createViewTransitionInstance --- ...eactFiberConfigFabricWithViewTransition.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index 0d8796927f2c..870310a2a5b7 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -40,8 +40,26 @@ export type RunningViewTransition = { ... }; +interface ViewTransitionPseudoElementType extends mixin$Animatable { + _scope: HTMLElement; + _selector: string; + getComputedStyle(): CSSStyleDeclaration; +} + +function ViewTransitionPseudoElement( + this: ViewTransitionPseudoElementType, + pseudo: string, + name: string, +) { + // TODO: Get the owner document from the root container. + this._pseudo = pseudo; + this._name = name; +} + export type ViewTransitionInstance = null | { name: string, + old: mixin$Animatable, + new: mixin$Animatable, ... }; @@ -179,7 +197,11 @@ export function addViewTransitionFinishedListener( export function createViewTransitionInstance( name: string, ): ViewTransitionInstance { - return {name}; + return { + name, + old: new (ViewTransitionPseudoElement: any)('old', name), + new: new (ViewTransitionPseudoElement: any)('new', name), + }; } export function applyViewTransitionName( From 7a1f5b6c80895449bd65a43dc80bf17b9de54717 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 6 Mar 2026 09:23:03 -0500 Subject: [PATCH 21/30] cleanup --- packages/react-art/src/ReactFiberConfigART.js | 1 - .../src/ReactFiberConfigFabric.js | 40 ------------------- .../src/ReactFiberConfigNative.js | 1 - .../src/ReactFiberConfigTestHost.js | 1 - 4 files changed, 43 deletions(-) diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 38628113b525..1ed9b04fa855 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -414,7 +414,6 @@ export const isPrimaryRenderer = false; export const warnsIfNotActing = false; export const supportsMutation = true; -export const supportsViewTransition = false; export function appendChild(parentInstance, child) { if (child.parentNode === parentInstance) { diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 5fe1961ab3ad..533b20fa6d74 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -168,46 +168,6 @@ export * from 'react-reconciler/src/ReactFiberConfigWithNoResources'; export * from 'react-reconciler/src/ReactFiberConfigWithNoSingletons'; export * from './ReactFiberConfigFabricWithViewTransition'; -// ------------------- -// Mutation -// (not supported) -// ------------------- - -function shim(...args: any): empty { - throw new Error( - 'The current renderer does not support mutation. ' + - 'This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); -} - -export const supportsMutation = false; - -export const cloneMutableInstance = shim; -export const cloneMutableTextInstance = shim; -export const appendChild = shim; -export const appendChildToContainer = shim; -export const commitTextUpdate = shim; - -export function commitMount( - instance: Instance, - type: string, - newProps: Props, - internalInstanceHandle: Object, -): void {} - -export const commitUpdate = shim; -export const insertBefore = shim; -export const insertInContainerBefore = shim; -export const removeChild = shim; -export const removeChildFromContainer = shim; -export const resetTextContent = shim; -export const hideInstance = shim; -export const hideTextInstance = shim; -export const unhideInstance = shim; -export const unhideTextInstance = shim; -export const clearContainer = shim; - export function appendInitialChild( parentInstance: Instance, child: Instance | TextInstance, diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index e76635ccf759..b5e086493a01 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -374,7 +374,6 @@ export function shouldAttemptEagerTransition(): boolean { // ------------------- export const supportsMutation = true; -export const supportsViewTransition = false; export function appendChild( parentInstance: Instance, diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index e750f7a2b0a7..417745828d60 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -274,7 +274,6 @@ export const noTimeout: -1 = -1; // ------------------- export const supportsMutation = true; -export const supportsViewTransition = false; export function commitUpdate( instance: Instance, From 76241bec65cee83f55096d745e14777380a9a28b Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 09:54:52 -0400 Subject: [PATCH 22/30] Remove supportsViewTransition config, let each renderer handle view transition stubs - Remove ReactFiberConfigWithNoViewTransition.js and supportsViewTransition flag - Restore inline view transition stubs in react-art, react-native, react-test-renderer - Guard Fabric view transition functions with enableViewTransitionForPersistenceMode - Enable enableViewTransitionForPersistenceMode for FB native --- packages/react-art/src/ReactFiberConfigART.js | 87 +++++++++- .../src/client/ReactFiberConfigDOM.js | 1 - ...eactFiberConfigFabricWithViewTransition.js | 64 +++++++- .../src/ReactFiberConfigNative.js | 150 +++++++++++++++++- .../src/ReactFiberCommitWork.js | 6 +- .../ReactFiberConfigWithNoViewTransition.js | 42 ----- .../src/forks/ReactFiberConfig.custom.js | 1 - .../src/ReactFiberConfigTestHost.js | 146 ++++++++++++++++- 8 files changed, 443 insertions(+), 54 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js index 1ed9b04fa855..50873af6da04 100644 --- a/packages/react-art/src/ReactFiberConfigART.js +++ b/packages/react-art/src/ReactFiberConfigART.js @@ -249,7 +249,6 @@ function applyTextProps(instance, props, prevProps = {}) { } } -export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; @@ -485,6 +484,92 @@ export function unhideTextInstance(textInstance, text): void { // Noop } +export function applyViewTransitionName(instance, name, className) { + // Noop +} + +export function restoreViewTransitionName(instance, props) { + // Noop +} + +export function cancelViewTransitionName(instance, name, props) { + // Noop +} + +export function cancelRootViewTransitionName(rootContainer) { + // Noop +} + +export function restoreRootViewTransitionName(rootContainer) { + // Noop +} + +export function cloneRootViewTransitionContainer(rootContainer) { + throw new Error('Not implemented.'); +} + +export function removeRootViewTransitionClone(rootContainer, clone) { + throw new Error('Not implemented.'); +} + +export type InstanceMeasurement = null; + +export function measureInstance(instance) { + return null; +} + +export function measureClonedInstance(instance) { + return null; +} + +export function wasInstanceInViewport(measurement): boolean { + return true; +} + +export function hasInstanceChanged(oldMeasurement, newMeasurement): boolean { + return false; +} + +export function hasInstanceAffectedParent( + oldMeasurement, + newMeasurement, +): boolean { + return false; +} + +export function startViewTransition() { + return null; +} + +export type RunningViewTransition = null; + +export function startGestureTransition() { + return null; +} + +export function stopViewTransition(transition: RunningViewTransition) {} + +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + callback(); +} + +export type ViewTransitionInstance = null | {name: string, ...}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return null; +} + +export type GestureTimeline = null; + +export function getCurrentGestureOffset(provider: GestureTimeline): number { + throw new Error('startGestureTransition is not yet supported in react-art.'); +} + export function clearContainer(container) { // TODO Implement this } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 41584e0840b8..727ec694bbf7 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -877,7 +877,6 @@ function handleErrorInNextTick(error: any) { // ------------------- export const supportsMutation = true; -export const supportsViewTransition = true; export function commitMount( domElement: Instance, diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index 870310a2a5b7..afbb616f0b60 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -16,6 +16,8 @@ import type { GestureTimeline, } from './ReactFiberConfigFabric'; +import {enableViewTransitionForPersistenceMode} from 'shared/ReactFeatureFlags'; + const { measureInstance: fabricMeasureInstance, applyViewTransitionName: fabricApplyViewTransitionName, @@ -24,7 +26,13 @@ const { cancelViewTransitionName: fabricCancelViewTransitionName, } = nativeFabricUIManager; -export const supportsViewTransition = true; +function shim(...args: any): empty { + throw new Error( + 'The current renderer does not support view transitions. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} export type InstanceMeasurement = { rect: {x: number, y: number, width: number, height: number}, @@ -67,6 +75,9 @@ export function restoreViewTransitionName( instance: Instance, props: Props, ): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } fabricRestoreViewTransitionName(instance.node); } @@ -76,16 +87,25 @@ export function cancelViewTransitionName( oldName: string, props: Props, ): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } fabricCancelViewTransitionName(instance.node, oldName); } export function cancelRootViewTransitionName(rootContainer: Container): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('cancelRootViewTransitionName is not implemented'); } } export function restoreRootViewTransitionName(rootContainer: Container): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('restoreRootViewTransitionName is not implemented'); } @@ -94,6 +114,9 @@ export function restoreRootViewTransitionName(rootContainer: Container): void { export function cloneRootViewTransitionContainer( rootContainer: Container, ): Instance { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('cloneRootViewTransitionContainer is not implemented'); } @@ -105,12 +128,18 @@ export function removeRootViewTransitionClone( rootContainer: Container, clone: Instance, ): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('removeRootViewTransitionClone is not implemented'); } } export function measureInstance(instance: Instance): InstanceMeasurement { + if (!enableViewTransitionForPersistenceMode) { + return; + } const measurement = fabricMeasureInstance(instance.node); return { rect: { @@ -121,11 +150,15 @@ export function measureInstance(instance: Instance): InstanceMeasurement { }, abs: false, clip: false, + // TODO: properly calculate whether instance is in viewport view: true, }; } export function measureClonedInstance(instance: Instance): InstanceMeasurement { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('measureClonedInstance is not implemented'); } @@ -140,6 +173,9 @@ export function measureClonedInstance(instance: Instance): InstanceMeasurement { export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { + if (!enableViewTransitionForPersistenceMode) { + return; + } return measurement.view; } @@ -147,6 +183,9 @@ export function hasInstanceChanged( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('hasInstanceChanged is not implemented'); } @@ -157,6 +196,9 @@ export function hasInstanceAffectedParent( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('hasInstanceAffectedParent is not implemented'); } @@ -175,6 +217,9 @@ export function startGestureTransition( errorCallback: (error: mixed) => void, finishedAnimation: () => void, ): RunningViewTransition { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('startGestureTransition is not implemented'); } @@ -182,6 +227,9 @@ export function startGestureTransition( } export function stopViewTransition(transition: RunningViewTransition): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } if (__DEV__) { console.warn('stopViewTransition is not implemented'); } @@ -191,12 +239,18 @@ export function addViewTransitionFinishedListener( transition: RunningViewTransition, callback: () => void, ): void { - callback(); + if (!enableViewTransitionForPersistenceMode) { + return; + } + transition.finished.finally(callback); } export function createViewTransitionInstance( name: string, ): ViewTransitionInstance { + if (!enableViewTransitionForPersistenceMode) { + return; + } return { name, old: new (ViewTransitionPseudoElement: any)('old', name), @@ -209,6 +263,9 @@ export function applyViewTransitionName( name: string, className: ?string, ): void { + if (!enableViewTransitionForPersistenceMode) { + return; + } // add view-transition-name to things that might animate for browser fabricApplyViewTransitionName(instance.node, name, className); } @@ -226,6 +283,9 @@ export function startViewTransition( blockedCallback: (name: string) => void, finishedAnimation: () => void, ): null | RunningViewTransition { + if (!enableViewTransitionForPersistenceMode) { + return; + } const transition = fabricStartViewTransition( // mutation () => { diff --git a/packages/react-native-renderer/src/ReactFiberConfigNative.js b/packages/react-native-renderer/src/ReactFiberConfigNative.js index b5e086493a01..404ae7a54a85 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigNative.js +++ b/packages/react-native-renderer/src/ReactFiberConfigNative.js @@ -8,6 +8,7 @@ */ import type {InspectorData, TouchedViewDataAtPoint} from './ReactNativeTypes'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; // Modules provided by RN: import { @@ -34,6 +35,8 @@ import { } from 'react-reconciler/src/ReactEventPriorities'; import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; + import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import type {ReactContext} from 'shared/ReactTypes'; @@ -109,7 +112,6 @@ function recursivelyUncacheFiberNode(node: Instance | TextInstance) { } } -export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoScopes'; @@ -589,6 +591,152 @@ export function unhideInstance(instance: Instance, props: Props): void { ); } +export function applyViewTransitionName( + instance: Instance, + name: string, + className: ?string, +): void { + // Not yet implemented +} + +export function restoreViewTransitionName( + instance: Instance, + props: Props, +): void { + // Not yet implemented +} + +export function cancelViewTransitionName( + instance: Instance, + name: string, + props: Props, +): void { + // Not yet implemented +} + +export function cancelRootViewTransitionName(rootContainer: Container): void { + // Not yet implemented +} + +export function restoreRootViewTransitionName(rootContainer: Container): void { + // Not yet implemented +} + +export function cloneRootViewTransitionContainer( + rootContainer: Container, +): Instance { + throw new Error('Not implemented.'); +} + +export function removeRootViewTransitionClone( + rootContainer: Container, + clone: Instance, +): void { + throw new Error('Not implemented.'); +} + +export type InstanceMeasurement = null; + +export function measureInstance(instance: Instance): InstanceMeasurement { + // This heuristic is better implemented at the native layer. + return null; +} + +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + return null; +} + +export function wasInstanceInViewport( + measurement: InstanceMeasurement, +): boolean { + return true; +} + +export function hasInstanceChanged( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + return false; +} + +export function hasInstanceAffectedParent( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + return false; +} + +export function startViewTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, + passiveCallback: () => mixed, + errorCallback: mixed => void, + blockedCallback: string => void, // Profiling-only + finishedAnimation: () => void, // Profiling-only +): null | RunningViewTransition { + mutationCallback(); + layoutCallback(); + // Skip afterMutationCallback(). We don't need it since we're not animating. + spawnedWorkCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } + // Skip passiveCallback(). Spawned work will schedule a task. + return null; +} + +export type RunningViewTransition = null; + +export function startGestureTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + timeline: GestureTimeline, + rangeStart: number, + rangeEnd: number, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + animateCallback: () => void, + errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only +): null | RunningViewTransition { + mutationCallback(); + animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } + return null; +} + +export function stopViewTransition(transition: RunningViewTransition) {} + +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + callback(); +} + +export type ViewTransitionInstance = null | {name: string, ...}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return null; +} + +export type GestureTimeline = null; + +export function getCurrentGestureOffset(provider: GestureTimeline): number { + throw new Error( + 'startGestureTransition is not yet supported in React Native.', + ); +} + export function clearContainer(container: Container): void { // TODO Implement this for React Native // UIManager does not expose a "remove all" type method. diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 934189b67b12..322c858bb9c0 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -158,7 +158,6 @@ import { supportsHydration, supportsResources, supportsSingletons, - supportsViewTransition, clearSuspenseBoundary, clearSuspenseBoundaryFromContainer, createContainerChildSet, @@ -3713,10 +3712,7 @@ function commitPassiveMountOnFiber( } if (isViewTransitionEligible) { - if ( - supportsViewTransition && - rootViewTransitionNameCanceled - ) { + if (supportsMutation && rootViewTransitionNameCanceled) { restoreRootViewTransitionName(finishedRoot.containerInfo); } } diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js deleted file mode 100644 index 471d969a5d8a..000000000000 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Renderers that don't support view transitions -// can re-export everything from this module. - -function shim(...args: any): empty { - throw new Error( - 'The current renderer does not support view transitions. ' + - 'This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); -} - -// View Transitions (when unsupported) -export const supportsViewTransition = false; -export const applyViewTransitionName = shim; -export const restoreViewTransitionName = shim; -export const cancelViewTransitionName = shim; -export const cancelRootViewTransitionName = shim; -export const restoreRootViewTransitionName = shim; -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; -export const startViewTransition = shim; -export type RunningViewTransition = null; -export const startGestureTransition = shim; -export const stopViewTransition = shim; -export const addViewTransitionFinishedListener = shim; -export type ViewTransitionInstance = null | {name: string, ...}; -export const createViewTransitionInstance = shim; diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 4a2d87e8c40c..1785fa9aaec9 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -73,7 +73,6 @@ export const warnsIfNotActing = $$$config.warnsIfNotActing; export const supportsMutation = $$$config.supportsMutation; export const supportsPersistence = $$$config.supportsPersistence; export const supportsHydration = $$$config.supportsHydration; -export const supportsViewTransition = $$$config.supportsViewTransition; export const getInstanceFromNode = $$$config.getInstanceFromNode; export const beforeActiveInstanceBlur = $$$config.beforeActiveInstanceBlur; export const afterActiveInstanceBlur = $$$config.afterActiveInstanceBlur; diff --git a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js index 417745828d60..6b04a36d297a 100644 --- a/packages/react-test-renderer/src/ReactFiberConfigTestHost.js +++ b/packages/react-test-renderer/src/ReactFiberConfigTestHost.js @@ -8,6 +8,8 @@ */ import type {ReactContext} from 'shared/ReactTypes'; +import type {TransitionTypes} from 'react/src/ReactTransitionType'; + import isArray from 'shared/isArray'; import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; import { @@ -15,6 +17,7 @@ import { NoEventPriority, type EventPriority, } from 'react-reconciler/src/ReactEventPriorities'; +import {enableProfilerTimer} from 'shared/ReactFeatureFlags'; export {default as rendererVersion} from 'shared/ReactVersion'; // TODO: Consider exporting the react-native version. export const rendererPackageName = 'react-test-renderer'; @@ -53,7 +56,6 @@ export type EventResponder = any; export type RendererInspectionConfig = $ReadOnly<{}>; export type TransitionStatus = mixed; -export * from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; export * from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; export * from 'react-reconciler/src/ReactFiberConfigWithNoHydration'; export * from 'react-reconciler/src/ReactFiberConfigWithNoTestSelectors'; @@ -330,6 +332,148 @@ export function unhideTextInstance( textInstance.isHidden = false; } +export function applyViewTransitionName( + instance: Instance, + name: string, + className: ?string, +): void { + // Noop +} + +export function restoreViewTransitionName( + instance: Instance, + props: Props, +): void { + // Noop +} + +export function cancelViewTransitionName( + instance: Instance, + name: string, + props: Props, +): void { + // Noop +} + +export function cancelRootViewTransitionName(rootContainer: Container): void { + // Noop +} + +export function restoreRootViewTransitionName(rootContainer: Container): void { + // Noop +} + +export function cloneRootViewTransitionContainer( + rootContainer: Container, +): Instance { + return { + type: 'ROOT', + props: {}, + isHidden: false, + children: [], + internalInstanceHandle: null, + rootContainerInstance: rootContainer, + tag: 'INSTANCE', + }; +} + +export function removeRootViewTransitionClone( + rootContainer: Container, + clone: Instance, +): void { + // Noop since it was never inserted anywhere. +} + +export type InstanceMeasurement = null; + +export function measureInstance(instance: Instance): InstanceMeasurement { + return null; +} + +export function measureClonedInstance(instance: Instance): InstanceMeasurement { + return null; +} + +export function wasInstanceInViewport( + measurement: InstanceMeasurement, +): boolean { + return true; +} + +export function hasInstanceChanged( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + return false; +} + +export function hasInstanceAffectedParent( + oldMeasurement: InstanceMeasurement, + newMeasurement: InstanceMeasurement, +): boolean { + return false; +} + +export function startViewTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + layoutCallback: () => void, + afterMutationCallback: () => void, + spawnedWorkCallback: () => void, + passiveCallback: () => mixed, + errorCallback: mixed => void, + blockedCallback: string => void, // Profiling-only + finishedAnimation: () => void, // Profiling-only +): null | RunningViewTransition { + mutationCallback(); + layoutCallback(); + // Skip afterMutationCallback(). We don't need it since we're not animating. + spawnedWorkCallback(); + // Skip passiveCallback(). Spawned work will schedule a task. + return null; +} + +export type RunningViewTransition = null; + +export function startGestureTransition( + suspendedState: null | SuspendedState, + rootContainer: Container, + timeline: GestureTimeline, + rangeStart: number, + rangeEnd: number, + transitionTypes: null | TransitionTypes, + mutationCallback: () => void, + animateCallback: () => void, + errorCallback: mixed => void, + finishedAnimation: () => void, // Profiling-only +): null | RunningViewTransition { + mutationCallback(); + animateCallback(); + if (enableProfilerTimer) { + finishedAnimation(); + } + return null; +} + +export function stopViewTransition(transition: RunningViewTransition) {} + +export function addViewTransitionFinishedListener( + transition: RunningViewTransition, + callback: () => void, +) { + callback(); +} + +export type ViewTransitionInstance = null | {name: string, ...}; + +export function createViewTransitionInstance( + name: string, +): ViewTransitionInstance { + return null; +} + export type FragmentInstanceType = null; export function createFragmentInstance( From e453b72d24eefa94ad1506d89c610f33a00befe7 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 10:02:19 -0400 Subject: [PATCH 23/30] Fix Fabric view transition types and restore shim() for disabled flag --- ...eactFiberConfigFabricWithViewTransition.js | 45 ++++++++++--------- scripts/flow/react-native-host-hooks.js | 7 +-- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index afbb616f0b60..c212448c010a 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -42,16 +42,14 @@ export type InstanceMeasurement = { }; export type RunningViewTransition = { - skipTransition(): void, finished: Promise, ready: Promise, ... }; interface ViewTransitionPseudoElementType extends mixin$Animatable { - _scope: HTMLElement; - _selector: string; - getComputedStyle(): CSSStyleDeclaration; + _pseudo: string; + _name: string; } function ViewTransitionPseudoElement( @@ -76,7 +74,7 @@ export function restoreViewTransitionName( props: Props, ): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } fabricRestoreViewTransitionName(instance.node); } @@ -88,14 +86,14 @@ export function cancelViewTransitionName( props: Props, ): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } fabricCancelViewTransitionName(instance.node, oldName); } export function cancelRootViewTransitionName(rootContainer: Container): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('cancelRootViewTransitionName is not implemented'); @@ -104,7 +102,7 @@ export function cancelRootViewTransitionName(rootContainer: Container): void { export function restoreRootViewTransitionName(rootContainer: Container): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('restoreRootViewTransitionName is not implemented'); @@ -115,7 +113,7 @@ export function cloneRootViewTransitionContainer( rootContainer: Container, ): Instance { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('cloneRootViewTransitionContainer is not implemented'); @@ -129,7 +127,7 @@ export function removeRootViewTransitionClone( clone: Instance, ): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('removeRootViewTransitionClone is not implemented'); @@ -138,7 +136,7 @@ export function removeRootViewTransitionClone( export function measureInstance(instance: Instance): InstanceMeasurement { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } const measurement = fabricMeasureInstance(instance.node); return { @@ -157,7 +155,7 @@ export function measureInstance(instance: Instance): InstanceMeasurement { export function measureClonedInstance(instance: Instance): InstanceMeasurement { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('measureClonedInstance is not implemented'); @@ -174,7 +172,7 @@ export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } return measurement.view; } @@ -184,7 +182,7 @@ export function hasInstanceChanged( newMeasurement: InstanceMeasurement, ): boolean { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('hasInstanceChanged is not implemented'); @@ -197,7 +195,7 @@ export function hasInstanceAffectedParent( newMeasurement: InstanceMeasurement, ): boolean { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('hasInstanceAffectedParent is not implemented'); @@ -218,17 +216,20 @@ export function startGestureTransition( finishedAnimation: () => void, ): RunningViewTransition { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('startGestureTransition is not implemented'); } - return null; + return { + finished: new Promise((resolve, reject)=>{}), + ready: new Promise((resolve, reject)=>{}), + }; } export function stopViewTransition(transition: RunningViewTransition): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } if (__DEV__) { console.warn('stopViewTransition is not implemented'); @@ -240,7 +241,7 @@ export function addViewTransitionFinishedListener( callback: () => void, ): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } transition.finished.finally(callback); } @@ -249,7 +250,7 @@ export function createViewTransitionInstance( name: string, ): ViewTransitionInstance { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } return { name, @@ -264,7 +265,7 @@ export function applyViewTransitionName( className: ?string, ): void { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } // add view-transition-name to things that might animate for browser fabricApplyViewTransitionName(instance.node, name, className); @@ -284,7 +285,7 @@ export function startViewTransition( finishedAnimation: () => void, ): null | RunningViewTransition { if (!enableViewTransitionForPersistenceMode) { - return; + shim(); } const transition = fabricStartViewTransition( // mutation diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index db8bbd9efaee..6d4e3a133f3c 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -314,9 +314,10 @@ declare const nativeFabricUIManager: { ) => void, startViewTransition: ( mutationCallback: () => void, - onReady: () => void, - onComplete: () => void, - ) => boolean, + ) => { + finished: Promise, + ready: Promise, + }, restoreViewTransitionName: (node: Object) => void, cancelViewTransitionName: (node: Object, oldName: string) => void, ... From 0c0ccdc8cabc84baa0bdc7fe2e9dad09de36a884 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 11:49:48 -0400 Subject: [PATCH 24/30] Simplify Fabric view transition stubs and enable enableViewTransition for FB native --- ...eactFiberConfigFabricWithViewTransition.js | 68 +++---------------- 1 file changed, 10 insertions(+), 58 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index c212448c010a..7b6542633e1c 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -19,11 +19,8 @@ import type { import {enableViewTransitionForPersistenceMode} from 'shared/ReactFeatureFlags'; const { - measureInstance: fabricMeasureInstance, applyViewTransitionName: fabricApplyViewTransitionName, startViewTransition: fabricStartViewTransition, - restoreViewTransitionName: fabricRestoreViewTransitionName, - cancelViewTransitionName: fabricCancelViewTransitionName, } = nativeFabricUIManager; function shim(...args: any): empty { @@ -73,10 +70,9 @@ export function restoreViewTransitionName( instance: Instance, props: Props, ): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); + if (__DEV__) { + console.warn('restoreViewTransitionName is not implemented'); } - fabricRestoreViewTransitionName(instance.node); } // Cancel the old and new snapshots of viewTransitionName @@ -85,25 +81,18 @@ export function cancelViewTransitionName( oldName: string, props: Props, ): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); + if (__DEV__) { + console.warn('cancelViewTransitionName is not implemented'); } - fabricCancelViewTransitionName(instance.node, oldName); } export function cancelRootViewTransitionName(rootContainer: Container): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('cancelRootViewTransitionName is not implemented'); } } export function restoreRootViewTransitionName(rootContainer: Container): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('restoreRootViewTransitionName is not implemented'); } @@ -112,9 +101,6 @@ export function restoreRootViewTransitionName(rootContainer: Container): void { export function cloneRootViewTransitionContainer( rootContainer: Container, ): Instance { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('cloneRootViewTransitionContainer is not implemented'); } @@ -126,25 +112,21 @@ export function removeRootViewTransitionClone( rootContainer: Container, clone: Instance, ): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('removeRootViewTransitionClone is not implemented'); } } export function measureInstance(instance: Instance): InstanceMeasurement { - if (!enableViewTransitionForPersistenceMode) { - shim(); + if (__DEV__) { + console.warn('measureInstance is not implemented'); } - const measurement = fabricMeasureInstance(instance.node); return { rect: { - x: measurement.x, - y: measurement.y, - width: measurement.width, - height: measurement.height, + x: 0, + y: 0, + width: 0, + height: 0, }, abs: false, clip: false, @@ -154,9 +136,6 @@ export function measureInstance(instance: Instance): InstanceMeasurement { } export function measureClonedInstance(instance: Instance): InstanceMeasurement { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('measureClonedInstance is not implemented'); } @@ -171,9 +150,6 @@ export function measureClonedInstance(instance: Instance): InstanceMeasurement { export function wasInstanceInViewport( measurement: InstanceMeasurement, ): boolean { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } return measurement.view; } @@ -181,9 +157,6 @@ export function hasInstanceChanged( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('hasInstanceChanged is not implemented'); } @@ -194,9 +167,6 @@ export function hasInstanceAffectedParent( oldMeasurement: InstanceMeasurement, newMeasurement: InstanceMeasurement, ): boolean { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('hasInstanceAffectedParent is not implemented'); } @@ -215,9 +185,6 @@ export function startGestureTransition( errorCallback: (error: mixed) => void, finishedAnimation: () => void, ): RunningViewTransition { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('startGestureTransition is not implemented'); } @@ -228,9 +195,6 @@ export function startGestureTransition( } export function stopViewTransition(transition: RunningViewTransition): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } if (__DEV__) { console.warn('stopViewTransition is not implemented'); } @@ -240,18 +204,12 @@ export function addViewTransitionFinishedListener( transition: RunningViewTransition, callback: () => void, ): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } transition.finished.finally(callback); } export function createViewTransitionInstance( name: string, ): ViewTransitionInstance { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } return { name, old: new (ViewTransitionPseudoElement: any)('old', name), @@ -264,9 +222,6 @@ export function applyViewTransitionName( name: string, className: ?string, ): void { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } // add view-transition-name to things that might animate for browser fabricApplyViewTransitionName(instance.node, name, className); } @@ -284,9 +239,6 @@ export function startViewTransition( blockedCallback: (name: string) => void, finishedAnimation: () => void, ): null | RunningViewTransition { - if (!enableViewTransitionForPersistenceMode) { - shim(); - } const transition = fabricStartViewTransition( // mutation () => { From f1a7a1e72575dfff930af2a50732223b12dd4825 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 12:37:02 -0400 Subject: [PATCH 25/30] Remove unused Fabric native view transition Flow declarations --- scripts/flow/react-native-host-hooks.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 6d4e3a133f3c..73c8ac79101b 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -301,12 +301,6 @@ declare const nativeFabricUIManager: { unstable_ContinuousEventPriority: number, unstable_IdleEventPriority: number, unstable_getCurrentEventPriority: () => number, - measureInstance: (node: Object) => { - x: number, - y: number, - width: number, - height: number, - }, applyViewTransitionName: ( node: Object, name: string, @@ -318,7 +312,5 @@ declare const nativeFabricUIManager: { finished: Promise, ready: Promise, }, - restoreViewTransitionName: (node: Object) => void, - cancelViewTransitionName: (node: Object, oldName: string) => void, ... }; From d11a0958f13b4eb8c9500014bed903d436eccc24 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 13:02:17 -0400 Subject: [PATCH 26/30] Remove unused imports and fix formatting --- .../ReactFiberConfigFabricWithViewTransition.js | 14 ++------------ scripts/flow/react-native-host-hooks.js | 4 +--- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index 7b6542633e1c..c74d821096b4 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -16,21 +16,11 @@ import type { GestureTimeline, } from './ReactFiberConfigFabric'; -import {enableViewTransitionForPersistenceMode} from 'shared/ReactFeatureFlags'; - const { applyViewTransitionName: fabricApplyViewTransitionName, startViewTransition: fabricStartViewTransition, } = nativeFabricUIManager; -function shim(...args: any): empty { - throw new Error( - 'The current renderer does not support view transitions. ' + - 'This error is likely caused by a bug in React. ' + - 'Please file an issue.', - ); -} - export type InstanceMeasurement = { rect: {x: number, y: number, width: number, height: number}, abs: boolean, @@ -189,8 +179,8 @@ export function startGestureTransition( console.warn('startGestureTransition is not implemented'); } return { - finished: new Promise((resolve, reject)=>{}), - ready: new Promise((resolve, reject)=>{}), + finished: new Promise((resolve, reject) => {}), + ready: new Promise((resolve, reject) => {}), }; } diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 73c8ac79101b..3e924f1c7f1b 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -306,9 +306,7 @@ declare const nativeFabricUIManager: { name: string, className: ?string, ) => void, - startViewTransition: ( - mutationCallback: () => void, - ) => { + startViewTransition: (mutationCallback: () => void) => { finished: Promise, ready: Promise, }, From 64d367ff7b7e66cc321bd8131e17d65f49134230 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 13:51:29 -0400 Subject: [PATCH 27/30] Silence cancelRootViewTransitionName and restoreRootViewTransitionName warnings --- .../src/ReactFiberConfigFabricWithViewTransition.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index c74d821096b4..bf9e779e7fb7 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -77,15 +77,11 @@ export function cancelViewTransitionName( } export function cancelRootViewTransitionName(rootContainer: Container): void { - if (__DEV__) { - console.warn('cancelRootViewTransitionName is not implemented'); - } + // No-op } export function restoreRootViewTransitionName(rootContainer: Container): void { - if (__DEV__) { - console.warn('restoreRootViewTransitionName is not implemented'); - } + // No-op } export function cloneRootViewTransitionContainer( From 2973299def778c1bb5447903f58b829ab45ae989 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 16:02:02 -0400 Subject: [PATCH 28/30] Fix startGestureTransition to return resolved promises instead of pending ones --- .../src/ReactFiberConfigFabricWithViewTransition.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index bf9e779e7fb7..cbe70a0d88f8 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -175,8 +175,8 @@ export function startGestureTransition( console.warn('startGestureTransition is not implemented'); } return { - finished: new Promise((resolve, reject) => {}), - ready: new Promise((resolve, reject) => {}), + finished: Promise.resolve(), + ready: Promise.resolve(), }; } From 50017bae984b1729e2a00fad2d8e114373ec0c6f Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 17:07:26 -0400 Subject: [PATCH 29/30] Add ReactFiberConfigWithNoViewTransition shim for noop renderer The noop renderer's mutationHostConfig includes view transition methods, but these keys weren't part of the Pick type, causing a Flow error. Instead of adding them to ReactFiberConfigWithNoMutation (which would conflict with Fabric's separate ReactFiberConfigFabricWithViewTransition re-exports), create a new ReactFiberConfigWithNoViewTransition shim file and union its keys into the noop renderer's mutationHostConfig type. --- .../src/createReactNoop.js | 4 +- .../ReactFiberConfigWithNoViewTransition.js | 41 +++++++++++++++++++ scripts/error-codes/codes.json | 3 +- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 4535580cd2cb..7172648f997a 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -25,6 +25,7 @@ import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities'; import type {TransitionTypes} from 'react/src/ReactTransitionType'; import typeof * as HostConfig from 'react-reconciler/src/ReactFiberConfig'; import typeof * as ReactFiberConfigWithNoMutation from 'react-reconciler/src/ReactFiberConfigWithNoMutation'; +import typeof * as ReactFiberConfigWithNoViewTransition from 'react-reconciler/src/ReactFiberConfigWithNoViewTransition'; import typeof * as ReactFiberConfigWithNoPersistence from 'react-reconciler/src/ReactFiberConfigWithNoPersistence'; import typeof * as ReconcilerAPI from 'react-reconciler/src/ReactFiberReconciler'; @@ -709,7 +710,8 @@ function createReactNoop( const mutationHostConfig: Pick< HostConfig, - $Keys, + | $Keys + | $Keys, > = { supportsMutation: true, diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js new file mode 100644 index 000000000000..cc8a82fa0edf --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +// Renderers that don't support view transitions +// can re-export everything from this module. + +function shim(...args: any): empty { + throw new Error( + 'The current renderer does not support view transitions. ' + + 'This error is likely caused by a bug in React. ' + + 'Please file an issue.', + ); +} + +// View Transitions (when unsupported) +export const applyViewTransitionName = shim; +export const restoreViewTransitionName = shim; +export const cancelViewTransitionName = shim; +export const cancelRootViewTransitionName = shim; +export const restoreRootViewTransitionName = shim; +export const cloneRootViewTransitionContainer = shim; +export const removeRootViewTransitionClone = shim; +export type InstanceMeasurement = any; +export const measureInstance = shim; +export const measureClonedInstance = shim; +export const wasInstanceInViewport = shim; +export const hasInstanceChanged = shim; +export const hasInstanceAffectedParent = shim; +export const startViewTransition = shim; +export type RunningViewTransition = any; +export const startGestureTransition = shim; +export const stopViewTransition = shim; +export const addViewTransitionFinishedListener = shim; +export type ViewTransitionInstance = any; +export const createViewTransitionInstance = shim; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 36cba98f5a9a..db92efc075ce 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -567,5 +567,6 @@ "579": "Invalid data for bytes stream.", "580": "Server Function has too many bound arguments. Received %s but the limit is %s.", "581": "BigInt is too large. Received %s digits but the limit is %s.", - "582": "Referenced Blob is not a Blob." + "582": "Referenced Blob is not a Blob.", + "583": "The current renderer does not support view transitions. This error is likely caused by a bug in React. Please file an issue." } From 3a5788785eb2cd2038d779ab84b4ee5b6e8b6075 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 19 Mar 2026 17:44:19 -0400 Subject: [PATCH 30/30] Retry CI