From bf9744b34e7330066f2972752e0975906055b33c Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Mar 2026 15:35:31 -0700 Subject: [PATCH 01/16] 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 a9edc0c84d..5a6b243f1f 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 322c858bb9..2b356f7218 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 ee5f22ab95..d574ad4fa1 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 bbb13a6eb1..bebae02dc8 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 = false; +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 6b0d934479..0a7dff59d5 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 954d9d88ea..315ea3d622 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 6bc80d2b8e..70db88d0d3 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 = false; +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 91dc33b28f..c99a477caa 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 = false; +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 76e3909cca..a07f344142 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 9989b47d3bf2608d2c62aafd6ab3b21878d5b470 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 14:13:56 -0500 Subject: [PATCH 02/16] 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 +---------------- scripts/error-codes/codes.json | 3 +- 9 files changed, 54 insertions(+), 402 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 50873af6da..1ed9b04fa8 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 4cb4e8e427..0f7394205a 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 404ae7a54a..b5e086493a 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 2b356f7218..36c670108c 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -159,6 +159,7 @@ import { supportsHydration, supportsResources, supportsSingletons, + supportsViewTransition, clearSuspenseBoundary, clearSuspenseBoundaryFromContainer, createContainerChildSet, @@ -3713,7 +3714,10 @@ function commitPassiveMountOnFiber( } if (isViewTransitionEligible) { - if (supportsMutation && rootViewTransitionNameCanceled) { + if ( + supportsViewTransition && + rootViewTransitionNameCanceled + ) { restoreRootViewTransitionName(finishedRoot.containerInfo); } else if ( enableViewTransitionForPersistenceMode && diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js b/packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js index 79cf3990a7..f243805214 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 0000000000..471d969a5d --- /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 1785fa9aae..4a2d87e8c4 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 6b04a36d29..417745828d 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( diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 09e60d8b25..2de374c316 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -566,5 +566,6 @@ "578": "Already initialized Iterator.", "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." + "581": "BigInt is too large. Received %s digits but the limit is %s.", + "582": "The current renderer does not support view transitions. This error is likely caused by a bug in React. Please file an issue." } From f4bbbbf0b7f8975b41f46712762105daf0fe38aa Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 14:13:56 -0500 Subject: [PATCH 03/16] 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 1ed9b04fa8..38628113b5 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 b5e086493a..e76635ccf7 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 417745828d..e750f7a2b0 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 cd1de67bfe296f2426437c99d5f05d68f6390c79 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Feb 2026 18:13:14 -0500 Subject: [PATCH 04/16] enableViewTransition for RN Summary: - turn on enableViewTransition feature - stub shim - run some mutation config fn at persistence mode too --- .../src/ReactFiberConfigFabric.js | 226 +++++++++++++++++- .../forks/ReactFeatureFlags.native-fb.js | 2 +- ...actFeatureFlags.test-renderer.native-fb.js | 2 +- 3 files changed, 222 insertions(+), 8 deletions(-) diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index 18c4eaddd9..7831299ef1 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, diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index bebae02dc8..db97d4b19b 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -68,7 +68,7 @@ export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; -export const enableViewTransition: boolean = false; +export const enableViewTransition: boolean = true; export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 70db88d0d3..8022dd8e22 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -54,7 +54,7 @@ export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive = false; export const enableThrottledScheduling = false; -export const enableViewTransition = false; +export const enableViewTransition = true; export const enableViewTransitionForPersistenceMode = false; export const enableGestureTransition = false; export const enableScrollEndPolyfill = true; From c8da8f1a126bf8eb653c6855bd80708c376bede8 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 6 Feb 2026 16:07:31 -0500 Subject: [PATCH 05/16] 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 7831299ef1..c0c4e224be 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 8f766ec0ae360785d3d509c1388bf1b865f24367 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 6 Feb 2026 17:42:16 -0500 Subject: [PATCH 06/16] 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 c0c4e224be..9e03d11e90 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 b0d81ee941d9bd15694c44391d1b57d3b2bf060b Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 11:59:03 -0500 Subject: [PATCH 07/16] 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 9e03d11e90..d8d2b385a3 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 9abfa29bfe4a8ffe7dd9199beff2d57a7ddc9f28 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 12:06:37 -0500 Subject: [PATCH 08/16] 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 d8d2b385a3..fc8780f688 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 227c78bca2..db8bbd9efa 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 5c37895e386878559593f5cac841698dddfb363b Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 12:14:35 -0500 Subject: [PATCH 09/16] turn off enableViewTransition for fb rn renderer by default --- packages/shared/forks/ReactFeatureFlags.native-fb.js | 2 +- .../shared/forks/ReactFeatureFlags.test-renderer.native-fb.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index db97d4b19b..bebae02dc8 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -68,7 +68,7 @@ export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive: boolean = false; export const enableThrottledScheduling: boolean = false; -export const enableViewTransition: boolean = true; +export const enableViewTransition: boolean = false; export const enableViewTransitionForPersistenceMode: boolean = false; export const enableGestureTransition: boolean = false; export const enableScrollEndPolyfill: boolean = true; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 8022dd8e22..70db88d0d3 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -54,7 +54,7 @@ export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; export const enableYieldingBeforePassive = false; export const enableThrottledScheduling = false; -export const enableViewTransition = true; +export const enableViewTransition = false; export const enableViewTransitionForPersistenceMode = false; export const enableGestureTransition = false; export const enableScrollEndPolyfill = true; From 0b0b55a432f7b6cd3a94fdf6fd0ffa044a32ae3a Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Feb 2026 12:21:29 -0500 Subject: [PATCH 10/16] 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 fc8780f688..24fc960b14 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 002edf092b04afc22f532156bd2cd99bf8ac5eee Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 13 Feb 2026 12:32:19 -0500 Subject: [PATCH 11/16] 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 24fc960b14..c5f58af377 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 db1dec5c8401949d7f46047f46ee74f1d0f8d2e9 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 25 Feb 2026 14:24:19 -0500 Subject: [PATCH 12/16] 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 c5f58af377..c53420bdf9 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 d3f5ddb15298c6333751529a5bfdc262f9861003 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 14:13:21 -0500 Subject: [PATCH 13/16] 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 c53420bdf9..5fe1961ab3 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 0000000000..0d8796927f --- /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 5f324eaaf0139af93b057fe7c84c1219838d06d3 Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Thu, 5 Mar 2026 16:04:33 -0500 Subject: [PATCH 14/16] 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 0d8796927f..870310a2a5 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 aff6b07c69c90a045337b659a9d463453b78741d Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Fri, 6 Mar 2026 09:23:03 -0500 Subject: [PATCH 15/16] 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 38628113b5..1ed9b04fa8 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 5fe1961ab3..533b20fa6d 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 e76635ccf7..b5e086493a 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 e750f7a2b0..417745828d 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 0d55141d036085a280ed7520c7a2fd6a0aa737ba Mon Sep 17 00:00:00 2001 From: Zeya Peng Date: Wed, 11 Mar 2026 14:36:44 -0700 Subject: [PATCH 16/16] Prevent view flattening for direct host children of ViewTransition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add finalizeViewTransitionChild renderer config function called during completeWork for HostComponents that are direct children of a ViewTransition boundary. Fabric implements it by injecting collapsable: false to prevent native view flattening. DOM is a no-op. Uses a stack cursor (viewTransitionCursor) to track whether the current fiber is inside a ViewTransition — ViewTransition pushes true, HostComponent pushes false (acting as a boundary). --- .../src/client/ReactFiberConfigDOM.js | 8 +++++ ...eactFiberConfigFabricWithViewTransition.js | 10 ++++++ .../src/ReactFiberBeginWork.js | 12 +++++++ .../src/ReactFiberCompleteWork.js | 30 ++++++++++++++--- .../ReactFiberConfigWithNoViewTransition.js | 1 + .../src/ReactFiberHostContext.js | 32 +++++++++++++++++++ .../src/ReactFiberUnwindWork.js | 18 ++++++++++- .../src/forks/ReactFiberConfig.custom.js | 2 ++ 8 files changed, 107 insertions(+), 6 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 0f7394205a..eb111c2bfd 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1498,6 +1498,14 @@ function countClientRects(rects: Array): number { return count; } +export function finalizeViewTransitionChild( + type: string, + props: Props, +): Props { + // No-op for DOM. View flattening is a React Native concept. + return props; +} + export function applyViewTransitionName( instance: Instance, name: string, diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js index 870310a2a5..6027e6e4fe 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabricWithViewTransition.js @@ -204,6 +204,16 @@ export function createViewTransitionInstance( }; } +export function finalizeViewTransitionChild( + type: string, + props: Props, +): Props { + // Prevent view flattening for direct host children of ViewTransition. + // Without this, Fabric's native-side optimization may remove the view + // from the platform hierarchy, breaking view transition animations. + return Object.assign({}, props, {collapsable: false}); +} + export function applyViewTransitionName( instance: Instance, name: string, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index cf2c082367..3bba42462d 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -188,6 +188,7 @@ import {shouldError, shouldSuspend} from './ReactFiberReconciler'; import { pushHostContext, pushHostContainer, + pushViewTransitionContext, getRootHostContainer, } from './ReactFiberHostContext'; import { @@ -3571,6 +3572,10 @@ function updateViewTransition( workInProgress: Fiber, renderLanes: Lanes, ) { + // Mark direct host children as being inside a ViewTransition so the renderer + // can finalize them (e.g. prevent view flattening in React Native). + pushViewTransitionContext(workInProgress); + if (workInProgress.stateNode === null) { // We previously reset the work-in-progress. // We need to create a new ViewTransitionState instance. @@ -4157,6 +4162,13 @@ function attemptEarlyBailoutIfNoScheduledUpdate( } // Fallthrough } + case ViewTransitionComponent: { + if (enableViewTransition) { + pushViewTransitionContext(workInProgress); + break; + } + // Fallthrough + } } return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 9b8c4a21bd..5300d93dc8 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -127,11 +127,14 @@ import { mayResourceSuspendCommit, preloadInstance, preloadResource, + finalizeViewTransitionChild, } from './ReactFiberConfig'; import { getRootHostContainer, popHostContext, getHostContext, + getIsInViewTransition, + popViewTransitionContext, popHostContainer, } from './ReactFiberHostContext'; import { @@ -1356,16 +1359,25 @@ function completeWork( case HostComponent: { popHostContext(workInProgress); const type = workInProgress.type; + + // After popping, check if this HostComponent is a direct host child + // of a ViewTransition. If so, let the renderer finalize the props + // (e.g. to prevent view flattening in React Native). + let instanceProps = newProps; + if (enableViewTransition && getIsInViewTransition()) { + instanceProps = finalizeViewTransitionChild(type, instanceProps); + } + if (current !== null && workInProgress.stateNode != null) { updateHostComponent( current, workInProgress, type, - newProps, + instanceProps, renderLanes, ); } else { - if (!newProps) { + if (!instanceProps) { if (workInProgress.stateNode === null) { throw new Error( 'We must have new props for new mounts. This error is likely ' + @@ -1397,7 +1409,7 @@ function completeWork( finalizeHydratedChildren( workInProgress.stateNode, type, - newProps, + instanceProps, currentHostContext, ) ) { @@ -1407,7 +1419,7 @@ function completeWork( const rootContainerInstance = getRootHostContainer(); const instance = createInstance( type, - newProps, + instanceProps, rootContainerInstance, currentHostContext, workInProgress, @@ -1425,7 +1437,7 @@ function completeWork( finalizeInitialChildren( instance, type, - newProps, + instanceProps, currentHostContext, ) ) { @@ -1433,6 +1445,13 @@ function completeWork( } } } + + // Ensure memoizedProps reflects the finalized props so that + // future renders diff against the correct props. + if (instanceProps !== newProps) { + workInProgress.memoizedProps = instanceProps; + } + bubbleProperties(workInProgress); if (enableViewTransition) { // Host Components act as their own View Transitions which doesn't run enter/exit animations. @@ -2056,6 +2075,7 @@ function completeWork( } case ViewTransitionComponent: { if (enableViewTransition) { + popViewTransitionContext(workInProgress); // We're a component that might need an exit transition. This flag will // bubble up to the parent tree to indicate that there's a child that // might need an exit View Transition upon unmount. diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js index 471d969a5d..1f428404ae 100644 --- a/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js +++ b/packages/react-reconciler/src/ReactFiberConfigWithNoViewTransition.js @@ -40,3 +40,4 @@ export const stopViewTransition = shim; export const addViewTransitionFinishedListener = shim; export type ViewTransitionInstance = null | {name: string, ...}; export const createViewTransitionInstance = shim; +export const finalizeViewTransitionChild = shim; diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js index 2d2ec4c88a..6395b16b10 100644 --- a/packages/react-reconciler/src/ReactFiberHostContext.js +++ b/packages/react-reconciler/src/ReactFiberHostContext.js @@ -23,9 +23,12 @@ import { NotPendingTransition, isPrimaryRenderer, } from './ReactFiberConfig'; +import {enableViewTransition} from 'shared/ReactFeatureFlags'; import {createCursor, push, pop} from './ReactFiberStack'; const contextStackCursor: StackCursor = createCursor(null); + +const viewTransitionCursor: StackCursor = createCursor(false); const contextFiberStackCursor: StackCursor = createCursor(null); const rootInstanceStackCursor: StackCursor = createCursor(null); @@ -93,7 +96,29 @@ function getHostContext(): HostContext { return context; } +function pushViewTransitionContext(fiber: Fiber): void { + if (enableViewTransition) { + push(viewTransitionCursor, true, fiber); + } +} + +function popViewTransitionContext(fiber: Fiber): void { + if (enableViewTransition) { + pop(viewTransitionCursor, fiber); + } +} + +function getIsInViewTransition(): boolean { + return viewTransitionCursor.current; +} + function pushHostContext(fiber: Fiber): void { + // HostComponents act as ViewTransition boundaries. Push false so that + // nested HostComponents below this one are not considered direct VT children. + if (enableViewTransition) { + push(viewTransitionCursor, false, fiber); + } + const stateHook: Hook | null = fiber.memoizedState; if (stateHook !== null) { // Propagate the current state to all the descendents. @@ -129,6 +154,10 @@ function pushHostContext(fiber: Fiber): void { } function popHostContext(fiber: Fiber): void { + if (enableViewTransition) { + pop(viewTransitionCursor, fiber); + } + if (contextFiberStackCursor.current === fiber) { // Do not pop unless this Fiber provided the current context. // pushHostContext() only pushes Fibers that provide unique contexts. @@ -159,10 +188,13 @@ function popHostContext(fiber: Fiber): void { export { getHostContext, + getIsInViewTransition, getCurrentRootHostContainer, getRootHostContainer, popHostContainer, popHostContext, pushHostContainer, pushHostContext, + pushViewTransitionContext, + popViewTransitionContext, }; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index b2954c41f5..31cd1d8648 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -33,15 +33,21 @@ import { LegacyHiddenComponent, CacheComponent, TracingMarkerComponent, + ViewTransitionComponent, } from './ReactWorkTags'; import {DidCapture, NoFlags, ShouldCapture, Update} from './ReactFiberFlags'; import {NoMode, ProfileMode} from './ReactTypeOfMode'; import { enableProfilerTimer, enableTransitionTracing, + enableViewTransition, } from 'shared/ReactFeatureFlags'; -import {popHostContainer, popHostContext} from './ReactFiberHostContext'; +import { + popHostContainer, + popHostContext, + popViewTransitionContext, +} from './ReactFiberHostContext'; import { popSuspenseListContext, popSuspenseHandler, @@ -243,6 +249,11 @@ function unwindWork( } } return null; + case ViewTransitionComponent: + if (enableViewTransition) { + popViewTransitionContext(workInProgress); + } + return null; default: return null; } @@ -324,6 +335,11 @@ function unwindInterruptedWork( } } break; + case ViewTransitionComponent: + if (enableViewTransition) { + popViewTransitionContext(interruptedWork); + } + break; default: break; } diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js index 4a2d87e8c4..ccefe1c053 100644 --- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js +++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js @@ -168,6 +168,8 @@ export const addViewTransitionFinishedListener = export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset; export const createViewTransitionInstance = $$$config.createViewTransitionInstance; +export const finalizeViewTransitionChild = + $$$config.finalizeViewTransitionChild; export const clearContainer = $$$config.clearContainer; export const createFragmentInstance = $$$config.createFragmentInstance; export const updateFragmentInstanceFiber =