From 2193e4941bbd3a6c6fc079fa646ca97c5ce6fda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 30 Oct 2026 05:38:49 -0700 Subject: [PATCH 1/6] Add experimental flag to enable props diffing only for core components Summary: Props diffing (`enablePropsUpdateReconciliationAndroid`) applies to every component whose props implement `getDiffProps`, which includes all codegen components. On view creation, props equal to their C++ default are not sent, so a native component whose default differs from the C++ default ends up with the wrong value. Third-party components have not been checked for this, which makes the existing flag unsafe to try in OSS. This adds `enableCoreComponentsPropsUpdateReconciliationAndroid`, released in the experimental stage, which enables props diffing only for the core components with hand-written props (View, Paragraph, Text, RawText, TextEffect, Image, ScrollView). All other components keep receiving raw props. TextInput is intentionally not included: on Android its component name is `AndroidTextInput` while its props report `TextInput` as the diffing target, so props diffing has never applied to it under either flag. Fixing that is left for a separate change. The decision now lives in a single non-virtual function, `isPropsDiffingEnabled`, used by both `FabricMountingManager::getProps` and `ConcreteComponentDescriptor::cloneProps`, so the two decisions cannot diverge. Behavior with the new flag off is unchanged. The Android ScrollViews (vertical, nested and horizontal) switch their native default `overflow` from `scroll` to `visible` under props diffing, because the C++ default `visible` is never sent. This switch only checked `enablePropsUpdateReconciliationAndroid`, so with only the new flag enabled ScrollViews clipped their content. The default is now resolved in one place, `ReactScrollViewHelper.getDefaultOverflow()`, which checks both flags. Changelog: [Android][Added] - Add experimental feature flag `enableCoreComponentsPropsUpdateReconciliationAndroid` to send prop updates based on props diffing for core components only Differential Revision: D127587361 --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +- ...agsOverrides_RNOSS_Experimental_Android.kt | 4 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../views/scroll/ReactHorizontalScrollView.kt | 14 +- .../views/scroll/ReactNestedScrollView.kt | 22 +-- .../react/views/scroll/ReactScrollView.kt | 20 +- .../views/scroll/ReactScrollViewHelper.kt | 14 ++ .../react/fabric/FabricMountingManager.cpp | 10 +- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../ReactScrollViewDefaultOverflowTest.kt | 74 ++++++++ .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 176 ++++++++++-------- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- ...tiveFeatureFlagsOverridesOSSExperimental.h | 6 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../view/tests/PropsDiffingGateTest.cpp | 99 ++++++++++ .../core/ConcreteComponentDescriptor.h | 10 +- .../ReactCommon/react/renderer/core/Props.cpp | 37 ++++ .../ReactCommon/react/renderer/core/Props.h | 8 + .../ReactNativeFeatureFlags.config.js | 11 ++ .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- .../api-snapshots/ReactAndroidDebugCxx.api | 1 + .../ReactAndroidFrameworksCxx.api | 1 + .../api-snapshots/ReactAndroidNewarchCxx.api | 1 + .../api-snapshots/ReactAndroidReleaseCxx.api | 1 + 36 files changed, 475 insertions(+), 160 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewDefaultOverflowTest.kt create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/tests/PropsDiffingGateTest.cpp diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7fcdaaeec16d..9d6c3c7ad338 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4630b5fe0cd8c2dd206179019b8a4ea3>> */ /** @@ -144,6 +144,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableBridgelessArchitecture(): Boolean = accessor.enableBridgelessArchitecture() + /** + * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. + */ + @JvmStatic + public fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean = accessor.enableCoreComponentsPropsUpdateReconciliationAndroid() + /** * Enable prop iterator setter-style construction of Props in C++ (this flag is not used in Java). */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 6e0da4d9d0e4..508f7ce8f96b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -39,6 +39,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null private var enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache: Boolean? = null private var enableBridgelessArchitectureCache: Boolean? = null + private var enableCoreComponentsPropsUpdateReconciliationAndroidCache: Boolean? = null private var enableCppPropsIteratorSetterCache: Boolean? = null private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null @@ -289,6 +290,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean { + var cached = enableCoreComponentsPropsUpdateReconciliationAndroidCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableCoreComponentsPropsUpdateReconciliationAndroid() + enableCoreComponentsPropsUpdateReconciliationAndroidCache = cached + } + return cached + } + override fun enableCppPropsIteratorSetter(): Boolean { var cached = enableCppPropsIteratorSetterCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2268d03f3674..06ccc040a0e2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6d86d4adbc261c9271b5bed72bd12dcf>> + * @generated SignedSource<<8e143d9668e06e97c1a4e04542e35295>> */ /** @@ -66,6 +66,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableBridgelessArchitecture(): Boolean + @DoNotStrip @JvmStatic public external fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean + @DoNotStrip @JvmStatic public external fun enableCppPropsIteratorSetter(): Boolean @DoNotStrip @JvmStatic public external fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index defafe46f6b4..7335882be3eb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<328892d289851df4d865c737199e8ee0>> */ /** @@ -61,6 +61,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableBridgelessArchitecture(): Boolean = true + override fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean = false + override fun enableCppPropsIteratorSetter(): Boolean = false override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 53af4b4edab0..83d2302aa943 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<19c3e5eaeb8f71fff3433783360a2a5f>> */ /** @@ -43,6 +43,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableAndroidTextMeasurementOptimizationsCache: Boolean? = null private var enableAsyncDiskCacheCheckInMultiSourceImageAndroidCache: Boolean? = null private var enableBridgelessArchitectureCache: Boolean? = null + private var enableCoreComponentsPropsUpdateReconciliationAndroidCache: Boolean? = null private var enableCppPropsIteratorSetterCache: Boolean? = null private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null @@ -312,6 +313,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean { + var cached = enableCoreComponentsPropsUpdateReconciliationAndroidCache + if (cached == null) { + cached = currentProvider.enableCoreComponentsPropsUpdateReconciliationAndroid() + accessedFeatureFlags.add("enableCoreComponentsPropsUpdateReconciliationAndroid") + enableCoreComponentsPropsUpdateReconciliationAndroidCache = cached + } + return cached + } + override fun enableCppPropsIteratorSetter(): Boolean { var cached = enableCppPropsIteratorSetterCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt index 850fcaa9ff55..c16af1df175e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -27,6 +27,8 @@ public open class ReactNativeFeatureFlagsOverrides_RNOSS_Experimental_Android : override fun disableIdleMountItemFrameCallbackRearmAndroid(): Boolean = true + override fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean = true + override fun enableFlexboxAutoMinSizeInStrictMode(): Boolean = true override fun preventShadowTreeCommitExhaustion(): Boolean = true diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8fa2f4269464..38e868007dbe 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<12a19d59f2a9d8242a0ae3d3d4b1d825>> */ /** @@ -61,6 +61,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableBridgelessArchitecture(): Boolean + @DoNotStrip public fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean + @DoNotStrip public fun enableCppPropsIteratorSetter(): Boolean @DoNotStrip public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt index 8714a01fa84a..becfbed38ebe 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.kt @@ -156,9 +156,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : // Private state private var activelyScrolling = false private var clippingRect: Rect? = null - private var _overflow: Overflow = - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE - else Overflow.SCROLL + private var _overflow: Overflow = ReactScrollViewHelper.getDefaultOverflow() private var dragging = false private var pagingEnabled = false private var postTouchRunnable: Runnable? = null @@ -262,9 +260,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : _virtualViewContainerState = null activelyScrolling = false clippingRect = null - _overflow = - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE - else Overflow.SCROLL + _overflow = ReactScrollViewHelper.getDefaultOverflow() dragging = false pagingEnabled = false postTouchRunnable = null @@ -425,11 +421,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : if (overflow == null) { Overflow.SCROLL } else { - Overflow.fromString( - overflow, - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE - else Overflow.SCROLL, - ) + Overflow.fromString(overflow, ReactScrollViewHelper.getDefaultOverflow()) } ImportantForInteractionHelper.setImportantForInteraction(this, pointerEvents, _overflow) invalidate() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt index 01cb197ff7b5..d93f15d7ef73 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactNestedScrollView.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5d7adbc00208fc9995b1c3e69c677a38>> + * @generated SignedSource<<5774837740e2ec0d6337db8b2c5e9ab9>> */ /** @@ -187,12 +187,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : private var activelyScrolling = false private var clippingRect: Rect? = null - private var _overflow: Overflow = - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) { - Overflow.VISIBLE - } else { - Overflow.SCROLL - } + private var _overflow: Overflow = ReactScrollViewHelper.getDefaultOverflow() private var dragging = false private var pagingEnabled = false private var postTouchRunnable: Runnable? = null @@ -234,12 +229,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : _virtualViewContainerState = null activelyScrolling = false clippingRect = null - _overflow = - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) { - Overflow.VISIBLE - } else { - Overflow.SCROLL - } + _overflow = ReactScrollViewHelper.getDefaultOverflow() dragging = false pagingEnabled = false postTouchRunnable = null @@ -392,11 +382,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : if (overflow == null) { Overflow.SCROLL } else { - Overflow.fromString( - overflow, - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE - else Overflow.SCROLL, - ) + Overflow.fromString(overflow, ReactScrollViewHelper.getDefaultOverflow()) } ImportantForInteractionHelper.setImportantForInteraction(this, pointerEvents, _overflow) invalidate() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt index fca6538d0774..2f5cfa049b83 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.kt @@ -179,12 +179,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : private var activelyScrolling = false private var clippingRect: Rect? = null - private var _overflow: Overflow = - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) { - Overflow.VISIBLE - } else { - Overflow.SCROLL - } + private var _overflow: Overflow = ReactScrollViewHelper.getDefaultOverflow() private var dragging = false private var pagingEnabled = false private var postTouchRunnable: Runnable? = null @@ -226,12 +221,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : _virtualViewContainerState = null activelyScrolling = false clippingRect = null - _overflow = - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) { - Overflow.VISIBLE - } else { - Overflow.SCROLL - } + _overflow = ReactScrollViewHelper.getDefaultOverflow() dragging = false pagingEnabled = false postTouchRunnable = null @@ -384,11 +374,7 @@ constructor(context: Context, private val fpsListener: FpsListener? = null) : if (overflow == null) { Overflow.SCROLL } else { - Overflow.fromString( - overflow, - if (ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid()) Overflow.VISIBLE - else Overflow.SCROLL, - ) + Overflow.fromString(overflow, ReactScrollViewHelper.getDefaultOverflow()) } ImportantForInteractionHelper.setImportantForInteraction(this, pointerEvents, _overflow) invalidate() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt index e6e0660d174b..1cbc5148ed89 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt @@ -24,12 +24,14 @@ import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap import com.facebook.react.common.ReactConstants import com.facebook.react.fabric.FabricUIManager +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel import com.facebook.react.uimanager.PixelUtil.toPixelFromDIP import com.facebook.react.uimanager.ReactClippingViewGroup import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.react.uimanager.style.Overflow import java.lang.ref.WeakReference import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs @@ -64,6 +66,18 @@ public object ReactScrollViewHelper { private var SMOOTH_SCROLL_DURATION = 250 private var smoothScrollDurationInitialized = false + // With props diffing, props equal to the C++ default (`visible`) are not sent, so the native + // default has to match it. + internal fun getDefaultOverflow(): Overflow = + if ( + ReactNativeFeatureFlags.enablePropsUpdateReconciliationAndroid() || + ReactNativeFeatureFlags.enableCoreComponentsPropsUpdateReconciliationAndroid() + ) { + Overflow.VISIBLE + } else { + Overflow.SCROLL + } + /** Shared by [ReactScrollView] and [ReactHorizontalScrollView]. */ @JvmStatic public fun emitScrollEvent(scrollView: T, xVelocity: Float, yVelocity: Float) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index b5f1eb569b2e..e2b2001a6477 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -329,15 +329,11 @@ jni::local_ref getProps( const ShadowView& oldShadowView, const ShadowView& newShadowView) { // We calculate the diffing between the props of the last mounted ShadowTree - // and the Props of the latest commited ShadowTree). ONLY for - // components when the "enablePropsUpdateReconciliationAndroid" feature flag - // is enabled. + // and the Props of the latest commited ShadowTree), for components selected + // by `isPropsDiffingEnabled`. auto* oldProps = oldShadowView.props.get(); auto* newProps = newShadowView.props.get(); - if ((ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid()) && - strcmp( - newShadowView.componentName, - newProps->getDiffPropsImplementationTarget()) == 0) { + if (isPropsDiffingEnabled(newShadowView.componentName, *newProps)) { auto diff = newProps->getDiffProps(oldProps); #ifdef REACT_NATIVE_DEBUG diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 23fd0978a766..d0725c39879f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<944781e7a547e601f0637869a3a3ec5e>> + * @generated SignedSource<> */ /** @@ -153,6 +153,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableCoreComponentsPropsUpdateReconciliationAndroid() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableCoreComponentsPropsUpdateReconciliationAndroid"); + return method(javaProvider_); + } + bool enableCppPropsIteratorSetter() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableCppPropsIteratorSetter"); @@ -720,6 +726,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableBridgelessArchitecture( return ReactNativeFeatureFlags::enableBridgelessArchitecture(); } +bool JReactNativeFeatureFlagsCxxInterop::enableCoreComponentsPropsUpdateReconciliationAndroid( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableCoreComponentsPropsUpdateReconciliationAndroid(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableCppPropsIteratorSetter( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableCppPropsIteratorSetter(); @@ -1198,6 +1209,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableBridgelessArchitecture", JReactNativeFeatureFlagsCxxInterop::enableBridgelessArchitecture), + makeNativeMethod( + "enableCoreComponentsPropsUpdateReconciliationAndroid", + JReactNativeFeatureFlagsCxxInterop::enableCoreComponentsPropsUpdateReconciliationAndroid), makeNativeMethod( "enableCppPropsIteratorSetter", JReactNativeFeatureFlagsCxxInterop::enableCppPropsIteratorSetter), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index ae774f80c82b..799b58768c34 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<23f91198ea1e19abb3eb7004de696505>> */ /** @@ -87,6 +87,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableBridgelessArchitecture( facebook::jni::alias_ref); + static bool enableCoreComponentsPropsUpdateReconciliationAndroid( + facebook::jni::alias_ref); + static bool enableCppPropsIteratorSetter( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewDefaultOverflowTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewDefaultOverflowTest.kt new file mode 100644 index 000000000000..184c16ce9ef3 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/views/scroll/ReactScrollViewDefaultOverflowTest.kt @@ -0,0 +1,74 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") + +package com.facebook.react.views.scroll + +import android.annotation.SuppressLint +import com.facebook.react.bridge.ReactTestHelper.createTestReactApplicationContext +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults +import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.testutils.shadows.ShadowSoLoader +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** Verifies the default overflow of the vertical and horizontal ScrollViews under props diffing. */ +@SuppressLint("DeprecatedClass", "DeprecatedMethod") +@RunWith(RobolectricTestRunner::class) +@Config(shadows = [ShadowSoLoader::class]) +class ReactScrollViewDefaultOverflowTest { + + @Before + fun setUp() { + ReactNativeFeatureFlagsForTests.setUp() + } + + @Test + fun defaultOverflowIsScrollWithoutPropsDiffing() { + overrideFlags(allComponents = false, coreComponents = false) + assertDefaultOverflow("scroll") + } + + @Test + fun defaultOverflowIsVisibleWithPropsDiffingForAllComponents() { + overrideFlags(allComponents = true, coreComponents = false) + assertDefaultOverflow("visible") + } + + @Test + fun defaultOverflowIsVisibleWithPropsDiffingForCoreComponents() { + overrideFlags(allComponents = false, coreComponents = true) + assertDefaultOverflow("visible") + } + + private fun assertDefaultOverflow(expected: String) { + val context = createTestReactApplicationContext() + val themedContext = ThemedReactContext(context, context, null, -1) + assertThat(ReactScrollViewManager().createViewInstance(themedContext).overflow) + .isEqualTo(expected) + assertThat(ReactHorizontalScrollViewManager().createViewInstance(themedContext).overflow) + .isEqualTo(expected) + } + + private fun overrideFlags(allComponents: Boolean, coreComponents: Boolean) { + ReactNativeFeatureFlags.dangerouslyForceOverride( + object : ReactNativeFeatureFlagsDefaults() { + override fun enablePropsUpdateReconciliationAndroid(): Boolean = allComponents + + override fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean = + coreComponents + }, + ) + } +} diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index ac8ddb96a112..2e4907a5748d 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<56e127471f63e47798ec619e6fa56738>> + * @generated SignedSource<<764aeebfb1e2fc17dae032247e5e69cf>> */ /** @@ -102,6 +102,10 @@ bool ReactNativeFeatureFlags::enableBridgelessArchitecture() { return getAccessor().enableBridgelessArchitecture(); } +bool ReactNativeFeatureFlags::enableCoreComponentsPropsUpdateReconciliationAndroid() { + return getAccessor().enableCoreComponentsPropsUpdateReconciliationAndroid(); +} + bool ReactNativeFeatureFlags::enableCppPropsIteratorSetter() { return getAccessor().enableCppPropsIteratorSetter(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013fb..08b1bf034df3 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6887b671526bc8a3b0678e3e45b4e3b4>> + * @generated SignedSource<<2651da9a855c0baafb3f856387b342f3>> */ /** @@ -136,6 +136,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableBridgelessArchitecture(); + /** + * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. + */ + RN_EXPORT static bool enableCoreComponentsPropsUpdateReconciliationAndroid(); + /** * Enable prop iterator setter-style construction of Props in C++ (this flag is not used in Java). */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418a..55db7d64ea83 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5d9712bd7e62ba3a29b084ba4c60bec5>> + * @generated SignedSource<<4b3db41e42b1dced34353a26a3c266c2>> */ /** @@ -371,6 +371,24 @@ bool ReactNativeFeatureFlagsAccessor::enableBridgelessArchitecture() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableCoreComponentsPropsUpdateReconciliationAndroid() { + auto flagValue = enableCoreComponentsPropsUpdateReconciliationAndroid_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(19, "enableCoreComponentsPropsUpdateReconciliationAndroid"); + + flagValue = currentProvider_->enableCoreComponentsPropsUpdateReconciliationAndroid(); + enableCoreComponentsPropsUpdateReconciliationAndroid_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableCppPropsIteratorSetter() { auto flagValue = enableCppPropsIteratorSetter_.load(); @@ -380,7 +398,7 @@ bool ReactNativeFeatureFlagsAccessor::enableCppPropsIteratorSetter() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(19, "enableCppPropsIteratorSetter"); + markFlagAsAccessed(20, "enableCppPropsIteratorSetter"); flagValue = currentProvider_->enableCppPropsIteratorSetter(); enableCppPropsIteratorSetter_ = flagValue; @@ -398,7 +416,7 @@ bool ReactNativeFeatureFlagsAccessor::enableCustomFocusSearchOnClippedElementsAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(20, "enableCustomFocusSearchOnClippedElementsAndroid"); + markFlagAsAccessed(21, "enableCustomFocusSearchOnClippedElementsAndroid"); flagValue = currentProvider_->enableCustomFocusSearchOnClippedElementsAndroid(); enableCustomFocusSearchOnClippedElementsAndroid_ = flagValue; @@ -416,7 +434,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDestroyShadowTreeRevisionAsync() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(21, "enableDestroyShadowTreeRevisionAsync"); + markFlagAsAccessed(22, "enableDestroyShadowTreeRevisionAsync"); flagValue = currentProvider_->enableDestroyShadowTreeRevisionAsync(); enableDestroyShadowTreeRevisionAsync_ = flagValue; @@ -434,7 +452,7 @@ bool ReactNativeFeatureFlagsAccessor::enableDoubleMeasurementFixAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(22, "enableDoubleMeasurementFixAndroid"); + markFlagAsAccessed(23, "enableDoubleMeasurementFixAndroid"); flagValue = currentProvider_->enableDoubleMeasurementFixAndroid(); enableDoubleMeasurementFixAndroid_ = flagValue; @@ -452,7 +470,7 @@ bool ReactNativeFeatureFlagsAccessor::enableEagerRootViewAttachment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(23, "enableEagerRootViewAttachment"); + markFlagAsAccessed(24, "enableEagerRootViewAttachment"); flagValue = currentProvider_->enableEagerRootViewAttachment(); enableEagerRootViewAttachment_ = flagValue; @@ -470,7 +488,7 @@ bool ReactNativeFeatureFlagsAccessor::enableExclusivePropsUpdateAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(24, "enableExclusivePropsUpdateAndroid"); + markFlagAsAccessed(25, "enableExclusivePropsUpdateAndroid"); flagValue = currentProvider_->enableExclusivePropsUpdateAndroid(); enableExclusivePropsUpdateAndroid_ = flagValue; @@ -488,7 +506,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranching() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(25, "enableFabricCommitBranching"); + markFlagAsAccessed(26, "enableFabricCommitBranching"); flagValue = currentProvider_->enableFabricCommitBranching(); enableFabricCommitBranching_ = flagValue; @@ -506,7 +524,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricCommitBranchingMergeOnMainThre // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(26, "enableFabricCommitBranchingMergeOnMainThread"); + markFlagAsAccessed(27, "enableFabricCommitBranchingMergeOnMainThread"); flagValue = currentProvider_->enableFabricCommitBranchingMergeOnMainThread(); enableFabricCommitBranchingMergeOnMainThread_ = flagValue; @@ -524,7 +542,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFabricLogs() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(27, "enableFabricLogs"); + markFlagAsAccessed(28, "enableFabricLogs"); flagValue = currentProvider_->enableFabricLogs(); enableFabricLogs_ = flagValue; @@ -542,7 +560,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFlexboxAutoMinSizeInStrictMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(28, "enableFlexboxAutoMinSizeInStrictMode"); + markFlagAsAccessed(29, "enableFlexboxAutoMinSizeInStrictMode"); flagValue = currentProvider_->enableFlexboxAutoMinSizeInStrictMode(); enableFlexboxAutoMinSizeInStrictMode_ = flagValue; @@ -560,7 +578,7 @@ bool ReactNativeFeatureFlagsAccessor::enableFollowUpMountItemDispatchAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(29, "enableFollowUpMountItemDispatchAndroid"); + markFlagAsAccessed(30, "enableFollowUpMountItemDispatchAndroid"); flagValue = currentProvider_->enableFollowUpMountItemDispatchAndroid(); enableFollowUpMountItemDispatchAndroid_ = flagValue; @@ -578,7 +596,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSCompressedTextFrameAdjustment() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(30, "enableIOSCompressedTextFrameAdjustment"); + markFlagAsAccessed(31, "enableIOSCompressedTextFrameAdjustment"); flagValue = currentProvider_->enableIOSCompressedTextFrameAdjustment(); enableIOSCompressedTextFrameAdjustment_ = flagValue; @@ -596,7 +614,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSTextBaselineOffsetPerLine() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(31, "enableIOSTextBaselineOffsetPerLine"); + markFlagAsAccessed(32, "enableIOSTextBaselineOffsetPerLine"); flagValue = currentProvider_->enableIOSTextBaselineOffsetPerLine(); enableIOSTextBaselineOffsetPerLine_ = flagValue; @@ -614,7 +632,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIOSViewClipToPaddingBox() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(32, "enableIOSViewClipToPaddingBox"); + markFlagAsAccessed(33, "enableIOSViewClipToPaddingBox"); flagValue = currentProvider_->enableIOSViewClipToPaddingBox(); enableIOSViewClipToPaddingBox_ = flagValue; @@ -632,7 +650,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImagePrefetchingAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(33, "enableImagePrefetchingAndroid"); + markFlagAsAccessed(34, "enableImagePrefetchingAndroid"); flagValue = currentProvider_->enableImagePrefetchingAndroid(); enableImagePrefetchingAndroid_ = flagValue; @@ -650,7 +668,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImageTransparentTintColor() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(34, "enableImageTransparentTintColor"); + markFlagAsAccessed(35, "enableImageTransparentTintColor"); flagValue = currentProvider_->enableImageTransparentTintColor(); enableImageTransparentTintColor_ = flagValue; @@ -668,7 +686,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImmediateUpdateModeForContentOffsetC // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(35, "enableImmediateUpdateModeForContentOffsetChanges"); + markFlagAsAccessed(36, "enableImmediateUpdateModeForContentOffsetChanges"); flagValue = currentProvider_->enableImmediateUpdateModeForContentOffsetChanges(); enableImmediateUpdateModeForContentOffsetChanges_ = flagValue; @@ -686,7 +704,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(36, "enableImperativeEvents"); + markFlagAsAccessed(37, "enableImperativeEvents"); flagValue = currentProvider_->enableImperativeEvents(); enableImperativeEvents_ = flagValue; @@ -704,7 +722,7 @@ bool ReactNativeFeatureFlagsAccessor::enableImperativeFocus() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(37, "enableImperativeFocus"); + markFlagAsAccessed(38, "enableImperativeFocus"); flagValue = currentProvider_->enableImperativeFocus(); enableImperativeFocus_ = flagValue; @@ -722,7 +740,7 @@ bool ReactNativeFeatureFlagsAccessor::enableInteropViewManagerClassLookUpOptimiz // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(38, "enableInteropViewManagerClassLookUpOptimizationIOS"); + markFlagAsAccessed(39, "enableInteropViewManagerClassLookUpOptimizationIOS"); flagValue = currentProvider_->enableInteropViewManagerClassLookUpOptimizationIOS(); enableInteropViewManagerClassLookUpOptimizationIOS_ = flagValue; @@ -740,7 +758,7 @@ bool ReactNativeFeatureFlagsAccessor::enableIntersectionObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(39, "enableIntersectionObserverByDefault"); + markFlagAsAccessed(40, "enableIntersectionObserverByDefault"); flagValue = currentProvider_->enableIntersectionObserverByDefault(); enableIntersectionObserverByDefault_ = flagValue; @@ -758,7 +776,7 @@ bool ReactNativeFeatureFlagsAccessor::enableKeyEvents() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(40, "enableKeyEvents"); + markFlagAsAccessed(41, "enableKeyEvents"); flagValue = currentProvider_->enableKeyEvents(); enableKeyEvents_ = flagValue; @@ -776,7 +794,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(41, "enableLayoutAnimationsOnAndroid"); + markFlagAsAccessed(42, "enableLayoutAnimationsOnAndroid"); flagValue = currentProvider_->enableLayoutAnimationsOnAndroid(); enableLayoutAnimationsOnAndroid_ = flagValue; @@ -794,7 +812,7 @@ bool ReactNativeFeatureFlagsAccessor::enableLayoutAnimationsOnIOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(42, "enableLayoutAnimationsOnIOS"); + markFlagAsAccessed(43, "enableLayoutAnimationsOnIOS"); flagValue = currentProvider_->enableLayoutAnimationsOnIOS(); enableLayoutAnimationsOnIOS_ = flagValue; @@ -812,7 +830,7 @@ bool ReactNativeFeatureFlagsAccessor::enableModuleArgumentNSNullConversionIOS() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(43, "enableModuleArgumentNSNullConversionIOS"); + markFlagAsAccessed(44, "enableModuleArgumentNSNullConversionIOS"); flagValue = currentProvider_->enableModuleArgumentNSNullConversionIOS(); enableModuleArgumentNSNullConversionIOS_ = flagValue; @@ -830,7 +848,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMountingCoordinatorPullModelAndroid( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(44, "enableMountingCoordinatorPullModelAndroid"); + markFlagAsAccessed(45, "enableMountingCoordinatorPullModelAndroid"); flagValue = currentProvider_->enableMountingCoordinatorPullModelAndroid(); enableMountingCoordinatorPullModelAndroid_ = flagValue; @@ -848,7 +866,7 @@ bool ReactNativeFeatureFlagsAccessor::enableMutationObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(45, "enableMutationObserverByDefault"); + markFlagAsAccessed(46, "enableMutationObserverByDefault"); flagValue = currentProvider_->enableMutationObserverByDefault(); enableMutationObserverByDefault_ = flagValue; @@ -866,7 +884,7 @@ bool ReactNativeFeatureFlagsAccessor::enableNativeCSSParsing() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(46, "enableNativeCSSParsing"); + markFlagAsAccessed(47, "enableNativeCSSParsing"); flagValue = currentProvider_->enableNativeCSSParsing(); enableNativeCSSParsing_ = flagValue; @@ -884,7 +902,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreallocatedPropsDiffOnInsertAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(47, "enablePreallocatedPropsDiffOnInsertAndroid"); + markFlagAsAccessed(48, "enablePreallocatedPropsDiffOnInsertAndroid"); flagValue = currentProvider_->enablePreallocatedPropsDiffOnInsertAndroid(); enablePreallocatedPropsDiffOnInsertAndroid_ = flagValue; @@ -902,7 +920,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePreparedTextLayout() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(48, "enablePreparedTextLayout"); + markFlagAsAccessed(49, "enablePreparedTextLayout"); flagValue = currentProvider_->enablePreparedTextLayout(); enablePreparedTextLayout_ = flagValue; @@ -920,7 +938,7 @@ bool ReactNativeFeatureFlagsAccessor::enablePropsUpdateReconciliationAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(49, "enablePropsUpdateReconciliationAndroid"); + markFlagAsAccessed(50, "enablePropsUpdateReconciliationAndroid"); flagValue = currentProvider_->enablePropsUpdateReconciliationAndroid(); enablePropsUpdateReconciliationAndroid_ = flagValue; @@ -938,7 +956,7 @@ bool ReactNativeFeatureFlagsAccessor::enableResizeObserverByDefault() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(50, "enableResizeObserverByDefault"); + markFlagAsAccessed(51, "enableResizeObserverByDefault"); flagValue = currentProvider_->enableResizeObserverByDefault(); enableResizeObserverByDefault_ = flagValue; @@ -956,7 +974,7 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(51, "enableSwiftUIBasedFilters"); + markFlagAsAccessed(52, "enableSwiftUIBasedFilters"); flagValue = currentProvider_->enableSwiftUIBasedFilters(); enableSwiftUIBasedFilters_ = flagValue; @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "enableViewCulling"); + markFlagAsAccessed(53, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "enableViewRecycling"); + markFlagAsAccessed(54, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "enableViewRecyclingForImage"); + markFlagAsAccessed(55, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(56, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "enableViewRecyclingForText"); + markFlagAsAccessed(57, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "enableViewRecyclingForView"); + markFlagAsAccessed(58, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(59, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::fixBorderlessRippleAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "fixBorderlessRippleAndroid"); + markFlagAsAccessed(60, "fixBorderlessRippleAndroid"); flagValue = currentProvider_->fixBorderlessRippleAndroid(); fixBorderlessRippleAndroid_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(61, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(62, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(63, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(64, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxEnabledRelease"); + markFlagAsAccessed(65, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(66, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(67, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(68, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::keepTimersRunningWhileHostPausedAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "keepTimersRunningWhileHostPausedAndroid"); + markFlagAsAccessed(69, "keepTimersRunningWhileHostPausedAndroid"); flagValue = currentProvider_->keepTimersRunningWhileHostPausedAndroid(); keepTimersRunningWhileHostPausedAndroid_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(70, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(71, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "perfIssuesEnabled"); + markFlagAsAccessed(72, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "perfMonitorV2Enabled"); + markFlagAsAccessed(73, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1352,7 +1370,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "preparedTextCacheSize"); + markFlagAsAccessed(74, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(75, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "redBoxV2Android"); + markFlagAsAccessed(76, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "redBoxV2IOS"); + markFlagAsAccessed(77, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(78, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(79, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(80, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "skipUnchangedChildrenInDifferentiator"); + markFlagAsAccessed(81, "skipUnchangedChildrenInDifferentiator"); flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); skipUnchangedChildrenInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useFabricInterop"); + markFlagAsAccessed(87, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNestedScrollViewAndroid"); + markFlagAsAccessed(89, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useSharedAnimatedBackend"); + markFlagAsAccessed(90, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTurboModuleInterop"); + markFlagAsAccessed(92, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "viewCullingOutsetRatio"); + markFlagAsAccessed(94, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1730,7 +1748,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewTransitionEnabled"); + markFlagAsAccessed(95, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1766,7 +1784,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "virtualViewPrerenderRatio"); + markFlagAsAccessed(97, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7a9bc59bc57e..0ef554bf15e3 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17f41335dce7e5b45e7af2878c47d69b>> + * @generated SignedSource<<917c49b8668e677e8e85ca97c8cb91db>> */ /** @@ -53,6 +53,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableAndroidTextMeasurementOptimizations(); bool enableAsyncDiskCacheCheckInMultiSourceImageAndroid(); bool enableBridgelessArchitecture(); + bool enableCoreComponentsPropsUpdateReconciliationAndroid(); bool enableCppPropsIteratorSetter(); bool enableCustomFocusSearchOnClippedElementsAndroid(); bool enableDestroyShadowTreeRevisionAsync(); @@ -142,7 +143,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 97> accessedFeatureFlags_; + std::array, 98> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -163,6 +164,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableAndroidTextMeasurementOptimizations_; std::atomic> enableAsyncDiskCacheCheckInMultiSourceImageAndroid_; std::atomic> enableBridgelessArchitecture_; + std::atomic> enableCoreComponentsPropsUpdateReconciliationAndroid_; std::atomic> enableCppPropsIteratorSetter_; std::atomic> enableCustomFocusSearchOnClippedElementsAndroid_; std::atomic> enableDestroyShadowTreeRevisionAsync_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882cc..8bcd29a48816 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<35e8175277baa31d135d802ef5a5e6f1>> + * @generated SignedSource<<2eaa9084184e082b495516961dff4c08>> */ /** @@ -105,6 +105,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return true; } + bool enableCoreComponentsPropsUpdateReconciliationAndroid() override { + return false; + } + bool enableCppPropsIteratorSetter() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfec..de4693a32c91 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -218,6 +218,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableBridgelessArchitecture(); } + bool enableCoreComponentsPropsUpdateReconciliationAndroid() override { + auto value = values_["enableCoreComponentsPropsUpdateReconciliationAndroid"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableCoreComponentsPropsUpdateReconciliationAndroid(); + } + bool enableCppPropsIteratorSetter() override { auto value = values_["enableCppPropsIteratorSetter"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h index 47292fd47cc4..33f32de8279a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsOverridesOSSExperimental.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<02c8f99ea2314241417752f2efbbefa3>> */ /** @@ -37,6 +37,10 @@ class ReactNativeFeatureFlagsOverridesOSSExperimental : public ReactNativeFeatur return true; } + bool enableCoreComponentsPropsUpdateReconciliationAndroid() override { + return true; + } + bool enableFlexboxAutoMinSizeInStrictMode() override { return true; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 664922287009..ca81245c47bc 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7413e958441d0502dc1266ff87e9da85>> + * @generated SignedSource<<4134897f2b8e7692103f86614f7ad284>> */ /** @@ -46,6 +46,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableAndroidTextMeasurementOptimizations() = 0; virtual bool enableAsyncDiskCacheCheckInMultiSourceImageAndroid() = 0; virtual bool enableBridgelessArchitecture() = 0; + virtual bool enableCoreComponentsPropsUpdateReconciliationAndroid() = 0; virtual bool enableCppPropsIteratorSetter() = 0; virtual bool enableCustomFocusSearchOnClippedElementsAndroid() = 0; virtual bool enableDestroyShadowTreeRevisionAsync() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adbf..64d965c296dd 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<65e9161ec3769f2c776bd50686c7edad>> */ /** @@ -139,6 +139,11 @@ bool NativeReactNativeFeatureFlags::enableBridgelessArchitecture( return ReactNativeFeatureFlags::enableBridgelessArchitecture(); } +bool NativeReactNativeFeatureFlags::enableCoreComponentsPropsUpdateReconciliationAndroid( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableCoreComponentsPropsUpdateReconciliationAndroid(); +} + bool NativeReactNativeFeatureFlags::enableCppPropsIteratorSetter( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableCppPropsIteratorSetter(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285a..66b31a57ffdb 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<90f271ea47f91e6a84979209b7b3d816>> + * @generated SignedSource<> */ /** @@ -76,6 +76,8 @@ class NativeReactNativeFeatureFlags bool enableBridgelessArchitecture(jsi::Runtime& runtime); + bool enableCoreComponentsPropsUpdateReconciliationAndroid(jsi::Runtime& runtime); + bool enableCppPropsIteratorSetter(jsi::Runtime& runtime); bool enableCustomFocusSearchOnClippedElementsAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/PropsDiffingGateTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/PropsDiffingGateTest.cpp new file mode 100644 index 000000000000..db48270aea40 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/PropsDiffingGateTest.cpp @@ -0,0 +1,99 @@ +/* + * 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. + */ + +#include + +#include + +#include +#include +#include + +#ifdef RN_SERIALIZABLE_STATE + +namespace facebook::react { + +namespace { + +class PropsDiffingFlags : public ReactNativeFeatureFlagsDefaults { + public: + PropsDiffingFlags(bool allComponents, bool coreComponents) + : allComponents_(allComponents), coreComponents_(coreComponents) {} + + bool enablePropsUpdateReconciliationAndroid() override { + return allComponents_; + } + + bool enableCoreComponentsPropsUpdateReconciliationAndroid() override { + return coreComponents_; + } + + private: + bool allComponents_; + bool coreComponents_; +}; + +// Stands in for codegen props, which return their own component name. +class CodegenLikeProps final : public ViewProps { + public: + ComponentName getDiffPropsImplementationTarget() const override { + return "Switch"; + } +}; + +} // namespace + +class PropsDiffingGateTest : public ::testing::Test { + protected: + void TearDown() override { + ReactNativeFeatureFlags::dangerouslyReset(); + } + + static void setFlags(bool allComponents, bool coreComponents) { + ReactNativeFeatureFlags::dangerouslyReset(); + ReactNativeFeatureFlags::override( + std::make_unique(allComponents, coreComponents)); + } + + const ViewProps viewProps_{}; + const CodegenLikeProps codegenProps_{}; +}; + +TEST_F(PropsDiffingGateTest, disabledWhenNoFlagIsOn) { + setFlags(/*allComponents*/ false, /*coreComponents*/ false); + EXPECT_FALSE(isPropsDiffingEnabled("View", viewProps_)); + EXPECT_FALSE(isPropsDiffingEnabled("Switch", codegenProps_)); +} + +TEST_F(PropsDiffingGateTest, allComponentsFlagEnablesCoreAndCodegen) { + setFlags(/*allComponents*/ true, /*coreComponents*/ false); + EXPECT_TRUE(isPropsDiffingEnabled("View", viewProps_)); + EXPECT_TRUE(isPropsDiffingEnabled("Switch", codegenProps_)); +} + +TEST_F(PropsDiffingGateTest, coreComponentsFlagEnablesOnlyCoreComponents) { + setFlags(/*allComponents*/ false, /*coreComponents*/ true); + EXPECT_TRUE(isPropsDiffingEnabled("View", viewProps_)); + EXPECT_FALSE(isPropsDiffingEnabled("Switch", codegenProps_)); +} + +TEST_F(PropsDiffingGateTest, disabledWhenComponentDoesNotImplementDiffing) { + // A component whose props inherit the View implementation without + // overriding the target falls back to raw props. + setFlags(/*allComponents*/ true, /*coreComponents*/ true); + EXPECT_FALSE(isPropsDiffingEnabled("SafeAreaView", viewProps_)); +} + +TEST_F(PropsDiffingGateTest, viewPropsTargetIsACoreComponent) { + setFlags(/*allComponents*/ false, /*coreComponents*/ true); + EXPECT_TRUE(isPropsDiffingEnabled( + viewProps_.getDiffPropsImplementationTarget(), viewProps_)); +} + +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/core/ConcreteComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/core/ConcreteComponentDescriptor.h index e11bf20fb3a1..7248441a536d 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ConcreteComponentDescriptor.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ConcreteComponentDescriptor.h @@ -156,13 +156,9 @@ class ConcreteComponentDescriptor : public ComponentDescriptor { ReactNativeFeatureFlags_DO_NOT_USE::enableAccumulatedUpdatesInRawPropsAndroid()) { // When exclusive props update is enabled, we only apply Props 1.5 processing // (raw props merging) when Props 2.0 is not available. - if (ReactNativeFeatureFlags_DO_NOT_USE::enablePropsUpdateReconciliationAndroid()) { - // Cast to base Props reference to safely call virtual method - const auto &baseProps = static_cast(*shadowNodeProps); - if (strcmp(ShadowNodeT::Name(), baseProps.getDiffPropsImplementationTarget()) == 0) { - // Props 2.0 supported for this component, Props 1.5 processing can be skipped - fallbackToDynamicRawPropsAccumulation = false; - } + if (isPropsDiffingEnabled(ShadowNodeT::Name(), static_cast(*shadowNodeProps))) { + // Props 2.0 supported for this component, Props 1.5 processing can be skipped + fallbackToDynamicRawPropsAccumulation = false; } } if (fallbackToDynamicRawPropsAccumulation) { diff --git a/packages/react-native/ReactCommon/react/renderer/core/Props.cpp b/packages/react-native/ReactCommon/react/renderer/core/Props.cpp index 3e8845e9c31d..4fd447fce3a1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/Props.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/Props.cpp @@ -13,6 +13,11 @@ #include #include +#include +#include +#include +#include + namespace facebook::react { Props::Props( @@ -59,6 +64,38 @@ void Props::initializeDynamicProps( ComponentName Props::getDiffPropsImplementationTarget() const { return ""; } + +namespace { + +// Components whose hand-written props implement `getDiffProps`. Codegen +// components are excluded: their native defaults are not guaranteed to match +// the C++ defaults that props diffing relies on. +bool isCoreComponentWithPropsDiffing(std::string_view componentName) { + static constexpr std::array kCoreComponents = { + "View", + "Paragraph", + "Text", + "RawText", + "TextEffect", + "Image", + "ScrollView", + }; + return std::find( + kCoreComponents.begin(), kCoreComponents.end(), componentName) != + kCoreComponents.end(); +} + +} // namespace + +bool isPropsDiffingEnabled(ComponentName componentName, const Props& props) { + bool isEnabledForComponent = + ReactNativeFeatureFlags::enablePropsUpdateReconciliationAndroid() || + (ReactNativeFeatureFlags:: + enableCoreComponentsPropsUpdateReconciliationAndroid() && + isCoreComponentWithPropsDiffing(componentName)); + return isEnabledForComponent && + std::strcmp(componentName, props.getDiffPropsImplementationTarget()) == 0; +} #endif #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactCommon/react/renderer/core/Props.h b/packages/react-native/ReactCommon/react/renderer/core/Props.h index c717e3182a05..c0e354ba60a4 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/Props.h +++ b/packages/react-native/ReactCommon/react/renderer/core/Props.h @@ -86,6 +86,14 @@ class Props : public virtual Sealable, public virtual DebugStringConvertible { #endif }; +#ifdef RN_SERIALIZABLE_STATE +/* + * Whether mount instructions for `componentName` are built with + * `Props::getDiffProps` instead of raw props, given the enabled feature flags. + */ +bool isPropsDiffingEnabled(ComponentName componentName, const Props &props); +#endif + namespace detail { /* diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22d..624ed089ca88 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -256,6 +256,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'stable', }, + enableCoreComponentsPropsUpdateReconciliationAndroid: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-26', + description: + 'When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'experimental', + }, enableCppPropsIteratorSetter: { dangerouslyExposeInPublicCppHeaders: true, defaultValue: false, diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff62..c88982c71fc7 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<674f975884ae746e067e614523f2bfc3>> + * @generated SignedSource<<8ea4462a11dc2fe429369bf6f71cc543>> * @flow strict * @noformat */ @@ -68,6 +68,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableAndroidTextMeasurementOptimizations: Getter, enableAsyncDiskCacheCheckInMultiSourceImageAndroid: Getter, enableBridgelessArchitecture: Getter, + enableCoreComponentsPropsUpdateReconciliationAndroid: Getter, enableCppPropsIteratorSetter: Getter, enableCustomFocusSearchOnClippedElementsAndroid: Getter, enableDestroyShadowTreeRevisionAsync: Getter, @@ -298,6 +299,10 @@ export const enableAsyncDiskCacheCheckInMultiSourceImageAndroid: Getter * Feature flag to enable the new bridgeless architecture. */ export const enableBridgelessArchitecture: Getter = createNativeFlagGetter('enableBridgelessArchitecture', true); +/** + * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. + */ +export const enableCoreComponentsPropsUpdateReconciliationAndroid: Getter = createNativeFlagGetter('enableCoreComponentsPropsUpdateReconciliationAndroid', false); /** * Enable prop iterator setter-style construction of Props in C++ (this flag is not used in Java). */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a9..07c69a14ab94 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2eb3f4a3dca013c51ad2913944855010>> + * @generated SignedSource<<8a6f75a66050ab8cf760cb8b4920cb0d>> * @flow strict * @noformat */ @@ -44,6 +44,7 @@ export interface Spec extends TurboModule { readonly enableAndroidTextMeasurementOptimizations?: () => boolean; readonly enableAsyncDiskCacheCheckInMultiSourceImageAndroid?: () => boolean; readonly enableBridgelessArchitecture?: () => boolean; + readonly enableCoreComponentsPropsUpdateReconciliationAndroid?: () => boolean; readonly enableCppPropsIteratorSetter?: () => boolean; readonly enableCustomFocusSearchOnClippedElementsAndroid?: () => boolean; readonly enableDestroyShadowTreeRevisionAsync?: () => boolean; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 675923cec7c0..5ca1d3de5784 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -490,6 +490,7 @@ YGDirection facebook::react::yogaDirectionFromLayoutDirection(facebook::react::L bool facebook::react::areBorderRadiiCircular(const facebook::react::BorderRadii& borderRadii); bool facebook::react::hostPlatformColorIsColorMeaningful(facebook::react::Color color) noexcept; bool facebook::react::isColorMeaningful(const facebook::react::SharedColor& color) noexcept; +bool facebook::react::isPropsDiffingEnabled(facebook::react::ComponentName componentName, const facebook::react::Props& props); bool facebook::react::isZero(facebook::react::Float n); constexpr auto facebook::react::to_underlying(auto e) noexcept; constexpr char facebook::react::toLower(char c); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 0d1f0fd94a91..86407003bb5a 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -704,6 +704,7 @@ bool facebook::react::areTextAttributesEquivalentLayoutWise(const facebook::reac bool facebook::react::hostPlatformColorIsColorMeaningful(facebook::react::Color color) noexcept; bool facebook::react::isColorMeaningful(const facebook::react::SharedColor& color) noexcept; bool facebook::react::isHermesBytecodeBundle(const facebook::react::BundleHeader& header); +bool facebook::react::isPropsDiffingEnabled(facebook::react::ComponentName componentName, const facebook::react::Props& props); bool facebook::react::isZero(facebook::react::Float n); const char* facebook::react::stringForScriptTag(const facebook::react::ScriptTag& tag); constexpr auto facebook::react::to_underlying(auto e) noexcept; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 4b15d66312e8..6605ed509dc0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -490,6 +490,7 @@ YGDirection facebook::react::yogaDirectionFromLayoutDirection(facebook::react::L bool facebook::react::areBorderRadiiCircular(const facebook::react::BorderRadii& borderRadii); bool facebook::react::hostPlatformColorIsColorMeaningful(facebook::react::Color color) noexcept; bool facebook::react::isColorMeaningful(const facebook::react::SharedColor& color) noexcept; +bool facebook::react::isPropsDiffingEnabled(facebook::react::ComponentName componentName, const facebook::react::Props& props); bool facebook::react::isZero(facebook::react::Float n); constexpr auto facebook::react::to_underlying(auto e) noexcept; constexpr char facebook::react::toLower(char c); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 4b15d66312e8..6605ed509dc0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -490,6 +490,7 @@ YGDirection facebook::react::yogaDirectionFromLayoutDirection(facebook::react::L bool facebook::react::areBorderRadiiCircular(const facebook::react::BorderRadii& borderRadii); bool facebook::react::hostPlatformColorIsColorMeaningful(facebook::react::Color color) noexcept; bool facebook::react::isColorMeaningful(const facebook::react::SharedColor& color) noexcept; +bool facebook::react::isPropsDiffingEnabled(facebook::react::ComponentName componentName, const facebook::react::Props& props); bool facebook::react::isZero(facebook::react::Float n); constexpr auto facebook::react::to_underlying(auto e) noexcept; constexpr char facebook::react::toLower(char c); From d62e7fb54ef311bb28a06b5f9eefae810d463043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 30 Oct 2026 05:38:50 -0700 Subject: [PATCH 2/6] Enable props diffing for TextInput Summary: `isPropsDiffingEnabled` only uses props diffing when a component's name matches the target returned by its props' `getDiffPropsImplementationTarget()`. On Android the TextInput component is named `AndroidTextInput`, but `AndroidTextInputProps` returned `TextInput`. The check always failed, so TextInput never used props diffing, even with `enablePropsUpdateReconciliationAndroid` on. It kept receiving raw props (or accumulated raw props) instead, and `AndroidTextInputProps::getDiffProps` was never called. This makes `AndroidTextInputProps` return `AndroidTextInput` and adds it to the core components covered by `enableCoreComponentsPropsUpdateReconciliationAndroid`. This changes behavior for anyone with `enablePropsUpdateReconciliationAndroid` enabled: TextInput now receives diffed props built by `AndroidTextInputProps::getDiffProps`, a code path that has not been exercised before. With both props diffing flags off, behavior is unchanged. Changelog: [Android][Fixed] - Fix props diffing never being applied to `TextInput` when props update reconciliation is enabled Differential Revision: D128563541 --- .../featureflags/ReactNativeFeatureFlags.kt | 4 +-- .../featureflags/ReactNativeFeatureFlags.h | 4 +-- .../AndroidTextInputProps.cpp | 2 +- .../tests/AndroidTextInputPropsTest.cpp | 25 +++++++++++++++++++ .../ReactCommon/react/renderer/core/Props.cpp | 3 ++- .../ReactNativeFeatureFlags.config.js | 2 +- .../featureflags/ReactNativeFeatureFlags.js | 4 +-- 7 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 9d6c3c7ad338..c15c65ba4677 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4630b5fe0cd8c2dd206179019b8a4ea3>> + * @generated SignedSource<<4004d8650ddb3d1bc9afb9c5c72fa191>> */ /** @@ -145,7 +145,7 @@ public object ReactNativeFeatureFlags { public fun enableBridgelessArchitecture(): Boolean = accessor.enableBridgelessArchitecture() /** - * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. + * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView, TextInput). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. */ @JvmStatic public fun enableCoreComponentsPropsUpdateReconciliationAndroid(): Boolean = accessor.enableCoreComponentsPropsUpdateReconciliationAndroid() diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 08b1bf034df3..652e976c8850 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2651da9a855c0baafb3f856387b342f3>> + * @generated SignedSource<<8fc79697c78a91d401a1aec0b5e44d1b>> */ /** @@ -137,7 +137,7 @@ class ReactNativeFeatureFlags { RN_EXPORT static bool enableBridgelessArchitecture(); /** - * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. + * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView, TextInput). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. */ RN_EXPORT static bool enableCoreComponentsPropsUpdateReconciliationAndroid(); diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index ce2a3ac666e6..1853a5519d3f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -354,7 +354,7 @@ SharedDebugStringConvertibleList AndroidTextInputProps::getDebugProps() const { #endif ComponentName AndroidTextInputProps::getDiffPropsImplementationTarget() const { - return "TextInput"; + return "AndroidTextInput"; } folly::dynamic AndroidTextInputProps::getDiffProps( diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp new file mode 100644 index 000000000000..8e520b6e7f74 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp @@ -0,0 +1,25 @@ +/* + * 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. + */ + +#include + +#if defined(ANDROID) && defined(RN_SERIALIZABLE_STATE) + +#include + +namespace facebook::react { + +// Props diffing is only used when the props target matches the component name. +TEST(AndroidTextInputPropsTest, diffPropsTargetMatchesComponentName) { + EXPECT_STREQ( + AndroidTextInputProps().getDiffPropsImplementationTarget(), + AndroidTextInputShadowNode::Name()); +} + +} // namespace facebook::react + +#endif diff --git a/packages/react-native/ReactCommon/react/renderer/core/Props.cpp b/packages/react-native/ReactCommon/react/renderer/core/Props.cpp index 4fd447fce3a1..f9680e04e0a1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/Props.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/Props.cpp @@ -71,7 +71,7 @@ namespace { // components are excluded: their native defaults are not guaranteed to match // the C++ defaults that props diffing relies on. bool isCoreComponentWithPropsDiffing(std::string_view componentName) { - static constexpr std::array kCoreComponents = { + static constexpr std::array kCoreComponents = { "View", "Paragraph", "Text", @@ -79,6 +79,7 @@ bool isCoreComponentWithPropsDiffing(std::string_view componentName) { "TextEffect", "Image", "ScrollView", + "AndroidTextInput", }; return std::find( kCoreComponents.begin(), kCoreComponents.end(), componentName) != diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 624ed089ca88..e896a8cb0f23 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -261,7 +261,7 @@ const definitions: FeatureFlagDefinitions = { metadata: { dateAdded: '2026-10-26', description: - 'When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled.', + 'When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView, TextInput). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled.', expectedReleaseValue: true, purpose: 'experimentation', }, diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index c88982c71fc7..13ca4b621462 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8ea4462a11dc2fe429369bf6f71cc543>> + * @generated SignedSource<<5a20b81ad6800e3a83cb4e9681e0f903>> * @flow strict * @noformat */ @@ -300,7 +300,7 @@ export const enableAsyncDiskCacheCheckInMultiSourceImageAndroid: Getter */ export const enableBridgelessArchitecture: Getter = createNativeFlagGetter('enableBridgelessArchitecture', true); /** - * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. + * When enabled, Android receives prop updates based on props diffing only for core components with hand-written props (View, Text, Image, ScrollView, TextInput). Other components keep receiving raw props unless `enablePropsUpdateReconciliationAndroid` is enabled. */ export const enableCoreComponentsPropsUpdateReconciliationAndroid: Getter = createNativeFlagGetter('enableCoreComponentsPropsUpdateReconciliationAndroid', false); /** From e4ffc5269efb5dcff80946402d1bc4d16fb45659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 30 Oct 2026 06:27:43 -0700 Subject: [PATCH 3/6] Align TextInput props diffing with native defaults Summary: With props diffing, a prop whose value equals its C++ default is not sent when the view is created, so the C++ default has to match the state of a newly created native view. Several `AndroidTextInputProps` fields did not: - `showSoftInputOnFocus` and `allowFontScaling` defaulted to `false` in C++ but are `true` natively, so setting them to `false` from JS had no effect. - `autoCorrect` was a plain `bool` defaulting to `false`, while the native setter has three states (on, off, unset). `autoCorrect={false}` was never sent and removing the prop could not restore the platform default. It is now `std::optional` and is sent as `null` when unset. - `numberOfLines`, `textBreakStrategy` and `includeFontPadding` were each emitted twice, once from the paragraph attributes and once from a duplicate flat field, with the later one overwriting the earlier. The flat `includeFontPadding` defaulted to `false` while the native default is `true`, so removing `includeFontPadding={true}` sent `false`. These keys are now only emitted from the paragraph attributes. The flat fields are not used for layout, which reads the paragraph and text attributes, so only the props sent to the native view change. Changelog: [Android][Fixed] - Fix `TextInput` props `showSoftInputOnFocus`, `allowFontScaling`, `autoCorrect` and `includeFontPadding` being ignored or reset incorrectly when props diffing is enabled Differential Revision: D128594780 --- .../AndroidTextInputProps.cpp | 24 ++++------- .../androidtextinput/AndroidTextInputProps.h | 7 ++-- .../tests/AndroidTextInputPropsTest.cpp | 40 +++++++++++++++++++ 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index 1853a5519d3f..4106efbe43e5 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -75,15 +75,15 @@ AndroidTextInputProps::AndroidTextInputProps( showSoftInputOnFocus(convertRawProp(context, rawProps, "showSoftInputOnFocus", sourceProps.showSoftInputOnFocus, - {false})), + {true})), autoCorrect(convertRawProp(context, rawProps, "autoCorrect", sourceProps.autoCorrect, - {false})), + {})), allowFontScaling(convertRawProp(context, rawProps, "allowFontScaling", sourceProps.allowFontScaling, - {false})), + {true})), maxFontSizeMultiplier(convertRawProp(context, rawProps, "maxFontSizeMultiplier", sourceProps.maxFontSizeMultiplier, @@ -290,7 +290,8 @@ folly::dynamic AndroidTextInputProps::getDynamic() const { props["importantForAutofill"] = importantForAutofill; props["showSoftInputOnFocus"] = showSoftInputOnFocus; props["autoCapitalize"] = autoCapitalize; - props["autoCorrect"] = autoCorrect; + props["autoCorrect"] = + autoCorrect.has_value() ? folly::dynamic(*autoCorrect) : nullptr; props["autoFocus"] = autoFocus; props["allowFontScaling"] = allowFontScaling; props["maxFontSizeMultiplier"] = maxFontSizeMultiplier; @@ -500,18 +501,10 @@ folly::dynamic AndroidTextInputProps::getDiffProps( result["returnKeyLabel"] = returnKeyLabel; } - if (numberOfLines != oldProps->numberOfLines) { - result["numberOfLines"] = numberOfLines; - } - if (disableFullscreenUI != oldProps->disableFullscreenUI) { result["disableFullscreenUI"] = disableFullscreenUI; } - if (textBreakStrategy != oldProps->textBreakStrategy) { - result["textBreakStrategy"] = textBreakStrategy; - } - if (inlineImageLeft != oldProps->inlineImageLeft) { result["inlineImageLeft"] = inlineImageLeft; } @@ -529,7 +522,8 @@ folly::dynamic AndroidTextInputProps::getDiffProps( } if (autoCorrect != oldProps->autoCorrect) { - result["autoCorrect"] = autoCorrect; + result["autoCorrect"] = + autoCorrect.has_value() ? folly::dynamic(*autoCorrect) : nullptr; } if (allowFontScaling != oldProps->allowFontScaling) { @@ -608,10 +602,6 @@ folly::dynamic AndroidTextInputProps::getDiffProps( result["textAlign"] = textAlign; } - if (includeFontPadding != oldProps->includeFontPadding) { - result["includeFontPadding"] = includeFontPadding; - } - if (fontWeight != oldProps->fontWeight) { result["fontWeight"] = fontWeight; } diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.h index 8bfc984e2529..78d8778c04df 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputProps.h @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace facebook::react { @@ -83,9 +84,9 @@ class AndroidTextInputProps final : public BaseTextInputProps { std::string inlineImageLeft{}; int inlineImagePadding{0}; std::string importantForAutofill{}; - bool showSoftInputOnFocus{false}; - bool autoCorrect{false}; - bool allowFontScaling{false}; + bool showSoftInputOnFocus{true}; + std::optional autoCorrect{}; + bool allowFontScaling{true}; Float maxFontSizeMultiplier{0.0}; std::string keyboardType{}; std::string returnKeyType{}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp index 8e520b6e7f74..40df56cfde0b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/tests/AndroidTextInputPropsTest.cpp @@ -20,6 +20,46 @@ TEST(AndroidTextInputPropsTest, diffPropsTargetMatchesComponentName) { AndroidTextInputShadowNode::Name()); } +// Values equal to the C++ default are not sent on creation, so the C++ default +// has to match the state of a newly created native view. +TEST(AndroidTextInputPropsTest, diffSendsValuesThatDifferFromNativeDefaults) { + AndroidTextInputProps props; + props.showSoftInputOnFocus = false; + props.allowFontScaling = false; + props.autoCorrect = false; + + auto diff = props.getDiffProps(nullptr); + + EXPECT_EQ(diff["showSoftInputOnFocus"], false); + EXPECT_EQ(diff["allowFontScaling"], false); + EXPECT_EQ(diff["autoCorrect"], false); +} + +TEST(AndroidTextInputPropsTest, diffOfDefaultPropsIsEmpty) { + EXPECT_TRUE(AndroidTextInputProps().getDiffProps(nullptr).empty()); +} + +TEST(AndroidTextInputPropsTest, removedAutoCorrectIsSentAsNull) { + AndroidTextInputProps oldProps; + oldProps.autoCorrect = false; + + auto diff = AndroidTextInputProps().getDiffProps(&oldProps); + + ASSERT_EQ(diff.count("autoCorrect"), 1); + EXPECT_TRUE(diff["autoCorrect"].isNull()); +} + +// `includeFontPadding` is sent from the paragraph attributes only, whose +// default matches the native default. +TEST(AndroidTextInputPropsTest, removedIncludeFontPaddingIsNotSentAsFalse) { + AndroidTextInputProps oldProps; + oldProps.includeFontPadding = true; + + auto diff = AndroidTextInputProps().getDiffProps(&oldProps); + + EXPECT_EQ(diff.count("includeFontPadding"), 0); +} + } // namespace facebook::react #endif From 6cfa5ee653d85217ca1ea8d3e23110403f9dfc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 30 Oct 2026 06:55:44 -0700 Subject: [PATCH 4/6] Stop sending unused "scroll" key in View props diff Summary: `HostPlatformViewProps::getDiffProps` sends a `scroll` key with the same value as `overflow` whenever clipping changes. No Android view manager has a `scroll` prop and nothing reads that key directly; every component reads `overflow`. `ViewProps.SCROLL` is the overflow value `"scroll"`, not a prop name. The key only adds a JNI string and a failed setter lookup per update, so remove it. Changelog: [Internal] Differential Revision: D128598698 --- .../react/renderer/components/view/HostPlatformViewProps.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp index 2f2820422161..9e8dfe5b442a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp @@ -651,7 +651,6 @@ folly::dynamic HostPlatformViewProps::getDiffProps( if (getClipsContentToBounds() != oldProps->getClipsContentToBounds()) { result["overflow"] = getClipsContentToBounds() ? "hidden" : "visible"; - result["scroll"] = result["overflow"]; } if (backfaceVisibility != oldProps->backfaceVisibility) { From 147eb480f314d454a49c530e55779170aa024bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 30 Oct 2026 08:11:05 -0700 Subject: [PATCH 5/6] Skip UpdateProps mount items with an empty props diff Summary: `executeMount` queues an `UpdateProps` mount item whenever the props pointer of a view changes, but with props diffing the diff was only computed later, while writing the instruction buffer. A new props object does not mean there is anything to update: layout-only changes (`width`, `flex`, `margin`, ...) create new props, but layout props are applied through `UpdateLayout`/`UpdatePadding` and are not part of the diff. Those items still created an empty `ReadableNativeMap` over JNI, used a buffer slot, and on the Java side allocated a `ReactStylesDiffMap` and ran `updateProperties` plus `onAfterUpdateTransaction` (accessibility update, `ReactImageView.maybeUpdateView`, `ReactTextView.updateView`, ...) without applying anything. The buffer size and item counts are fixed before the buffer is written, so the diff is now computed when the mount item is created and the item is dropped when the diff is empty. The computed diff is kept in the item (`CppMountItem::diffProps`, a `unique_ptr` so items cannot be copied by accident) and moved into the Java map when the buffer is written, so every diff is still computed exactly once, under the same lock as before. This applies to every `UpdateProps` item created in `executeMount` (update, and the insert paths for preallocated views). Skipping an empty map applies the same setters as sending it (none); the differences are that `onAfterUpdateTransaction` no longer runs and `ViewState.currentProps` keeps the previous props map instead of an empty one. Components without props diffing (and builds with props diffing off) are unchanged. Create items are unchanged. Changelog: [Internal] Differential Revision: D128610033 --- .../react/fabric/FabricMountingManager.cpp | 130 +++++++++++------- .../src/main/jni/react/fabric/MountItem.h | 6 + 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index e2b2001a6477..ab4605824b57 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -325,38 +326,45 @@ bool shouldDiffInsertPropsAgainstPreallocatedProps() { ReactNativeFeatureFlags::enablePreallocatedPropsDiffOnInsertAndroid(); } -jni::local_ref getProps( +// Diffs the props of the last mounted ShadowTree against the props of the +// latest committed ShadowTree. Only valid when `isPropsDiffingEnabled`. +folly::dynamic computeDiffProps( const ShadowView& oldShadowView, const ShadowView& newShadowView) { - // We calculate the diffing between the props of the last mounted ShadowTree - // and the Props of the latest commited ShadowTree), for components selected - // by `isPropsDiffingEnabled`. auto* oldProps = oldShadowView.props.get(); auto* newProps = newShadowView.props.get(); - if (isPropsDiffingEnabled(newShadowView.componentName, *newProps)) { - auto diff = newProps->getDiffProps(oldProps); + auto diff = newProps->getDiffProps(oldProps); #ifdef REACT_NATIVE_DEBUG - if (oldProps != nullptr) { - auto controlDiff = - diffDynamicProps(oldProps->rawProps, newProps->rawProps); - - for (const auto& [prop, value] : controlDiff.items()) { - if (diff.count(prop) == 0) { - // Skip layout-only props since they are not included in Props 2.0 - if (!isLayoutOnlyProp(prop.asString())) { - LOG(ERROR) << "Props diff validation failed: Props 1.5 has prop '" - << prop.asString() - << "' = " << (value != nullptr ? value : "NULL") - << " that Props 2.0 doesn't have for component " - << newShadowView.componentName; - } + if (oldProps != nullptr) { + auto controlDiff = diffDynamicProps(oldProps->rawProps, newProps->rawProps); + + for (const auto& [prop, value] : controlDiff.items()) { + if (diff.count(prop) == 0) { + // Skip layout-only props since they are not included in Props 2.0 + if (!isLayoutOnlyProp(prop.asString())) { + LOG(ERROR) << "Props diff validation failed: Props 1.5 has prop '" + << prop.asString() + << "' = " << (value != nullptr ? value : "NULL") + << " that Props 2.0 doesn't have for component " + << newShadowView.componentName; } } } + } #endif - return ReadableNativeMap::newObjectCxxArgs(std::move(diff)); + return diff; +} + +jni::local_ref getProps( + const ShadowView& oldShadowView, + const ShadowView& newShadowView) { + auto* oldProps = oldShadowView.props.get(); + auto* newProps = newShadowView.props.get(); + if (isPropsDiffingEnabled(newShadowView.componentName, *newProps)) { + return ReadableNativeMap::newObjectCxxArgs( + computeDiffProps(oldShadowView, newShadowView)); } if (ReactNativeFeatureFlags::enableAccumulatedUpdatesInRawPropsAndroid()) { if (oldProps == nullptr) { @@ -369,6 +377,24 @@ jni::local_ref getProps( return ReadableNativeMap::newObjectCxxArgs(newProps->rawProps); } +// With props diffing, returns no item when there is nothing to update (e.g. +// only layout props changed) so it is not sent to Java and applied as a no-op. +std::optional makeUpdatePropsMountItem( + const ShadowView& oldShadowView, + const ShadowView& newShadowView) { + auto mountItem = + CppMountItem::UpdatePropsMountItem(oldShadowView, newShadowView); + if (isPropsDiffingEnabled( + newShadowView.componentName, *newShadowView.props)) { + auto diff = computeDiffProps(oldShadowView, newShadowView); + if (diff.empty()) { + return std::nullopt; + } + mountItem.diffProps = std::make_unique(std::move(diff)); + } + return mountItem; +} + struct InstructionBuffer { InstructionBuffer(int intsSize, int objectsSize) : objects(jni::JArrayClass::newArray(objectsSize)) { @@ -480,11 +506,17 @@ inline void writeRemoveMountItem( inline void writeUpdatePropsMountItem( InstructionBuffer& buffer, - const CppMountItem& mountItem) { + CppMountItem& mountItem) { buffer.writeInt(mountItem.newChildShadowView.tag); - buffer.writeObject( - getProps(mountItem.oldChildShadowView, mountItem.newChildShadowView) - .get()); + if (mountItem.diffProps != nullptr) { + buffer.writeObject( + ReadableNativeMap::newObjectCxxArgs(std::move(*mountItem.diffProps)) + .get()); + } else { + buffer.writeObject( + getProps(mountItem.oldChildShadowView, mountItem.newChildShadowView) + .get()); + } } inline void writeUpdateStateMountItem( @@ -686,11 +718,12 @@ void FabricMountingManager::executeMount( } if (oldChildShadowView.props != newChildShadowView.props) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - oldChildShadowView, newChildShadowView)); + if (auto mountItem = makeUpdatePropsMountItem( + oldChildShadowView, newChildShadowView)) { + (maintainMutationOrder ? cppCommonMountItems + : cppUpdatePropsMountItems) + .push_back(std::move(*mountItem)); + } } if (oldChildShadowView.state != newChildShadowView.state) { (maintainMutationOrder ? cppCommonMountItems @@ -771,22 +804,24 @@ void FabricMountingManager::executeMount( } if (shouldCreateView || !shouldDiffInsertPropsAgainstPreallocatedProps()) { - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - {}, newChildShadowView)); + if (auto mountItem = + makeUpdatePropsMountItem({}, newChildShadowView)) { + (maintainMutationOrder ? cppCommonMountItems + : cppUpdatePropsMountItems) + .push_back(std::move(*mountItem)); + } } else if (auto& preallocatedProps = allocatedView->second.preallocatedProps; preallocatedProps != nullptr) { if (preallocatedProps != newChildShadowView.props) { auto preallocatedShadowView = newChildShadowView; preallocatedShadowView.props = preallocatedProps; - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - preallocatedShadowView, newChildShadowView)); + if (auto mountItem = makeUpdatePropsMountItem( + preallocatedShadowView, newChildShadowView)) { + (maintainMutationOrder ? cppCommonMountItems + : cppUpdatePropsMountItems) + .push_back(std::move(*mountItem)); + } } preallocatedProps = nullptr; } @@ -794,11 +829,12 @@ void FabricMountingManager::executeMount( if (shouldCreateView) { LOG(ERROR) << "Emitting insert for unallocated view " << newChildShadowView.tag; - (maintainMutationOrder ? cppCommonMountItems - : cppUpdatePropsMountItems) - .push_back( - CppMountItem::UpdatePropsMountItem( - {}, newChildShadowView)); + if (auto mountItem = + makeUpdatePropsMountItem({}, newChildShadowView)) { + (maintainMutationOrder ? cppCommonMountItems + : cppUpdatePropsMountItems) + .push_back(std::move(*mountItem)); + } } } @@ -914,7 +950,7 @@ void FabricMountingManager::executeMount( // Fill in CREATE instructions. for (int i = 0; i < cppCommonMountItems.size(); i++) { - const auto& mountItem = cppCommonMountItems[i]; + auto& mountItem = cppCommonMountItems[i]; const auto& mountItemType = mountItem.type; // Get type here, and count forward how many items of this type are in a @@ -972,7 +1008,7 @@ void FabricMountingManager::executeMount( buffer, CppMountItem::Type::UpdateProps, cppUpdatePropsMountItems.size()); - for (const auto& mountItem : cppUpdatePropsMountItems) { + for (auto& mountItem : cppUpdatePropsMountItems) { writeUpdatePropsMountItem(buffer, mountItem); } } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/MountItem.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/MountItem.h index 0901de24f001..d6015d59b5c8 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/MountItem.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/MountItem.h @@ -8,8 +8,11 @@ #pragma once #include +#include #include +#include + namespace facebook::react { struct JMountItem : public jni::JavaClass { @@ -63,6 +66,9 @@ struct CppMountItem final { ShadowView oldChildShadowView = {}; ShadowView newChildShadowView = {}; int index = {}; + + // For UpdateProps items, the props diff when it was already computed. + std::unique_ptr diffProps = {}; }; } // namespace facebook::react From 306ccb36a24fa12b4d58be08fece3d1299f326ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ma=C5=82ecki?= Date: Fri, 30 Oct 2026 09:24:30 -0700 Subject: [PATCH 6/6] Avoid redundant work in View props diffing Summary: Small cleanups in `HostPlatformViewProps::getDiffProps`, which runs for every View update with props diffing and is also called by the Image, ScrollView, Paragraph and TextInput diffs: - The diff helpers (`updateEventProp`, `updateBorderWidthPropValue`, `updateBorderRadiusPropValue`, `updateBorderColorPropValue`, `updateNativeDrawableProp`) took the prop name as `const std::string&` but are always called with literals, so every call built a temporary `std::string`, also for unchanged values. Names longer than the small-string buffer (e.g. `borderBottomRightRadius`, `onStartShouldSetResponderCapture`) allocated on the heap. They now take `const char*`. - A `transformOrigin`-only change rebuilt and resent the whole `transform` array. `BaseViewManager.setTransformOrigin` already marks the transform invalid and `onAfterUpdateTransaction` re-applies the stored transform, so `transform` is now only sent when it changes. For a View that sets only `transformOrigin`, Java now keeps a `null` transform instead of receiving `[]`; both reset the view to the identity transform. - Built `folly::dynamic` arrays/objects (accessibility actions, value, labelled-by, order, native drawables) were copied into the result instead of moved. Changelog: [Internal] Differential Revision: D128623431 --- .../components/view/HostPlatformViewProps.cpp | 28 +++++++++-------- .../view/tests/ViewPropsDiffTest.cpp | 31 +++++++++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp index 9e8dfe5b442a..246cb9db6e16 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp @@ -143,7 +143,7 @@ inline static void updateEventProp( const ViewEvents& newEvents, const ViewEvents& oldEvents, const ViewEvents::Offset& offset, - const std::string& name) { + const char* name) { if (newEvents[offset] != oldEvents[offset]) { auto value = newEvents[offset]; result[name] = value; @@ -152,7 +152,7 @@ inline static void updateEventProp( static void updateBorderWidthPropValue( folly::dynamic& result, - const std::string& propName, + const char* propName, const std::optional& newValue, const std::optional& oldValue) { if (newValue != oldValue) { @@ -189,7 +189,7 @@ static void updateBorderWidthProps( static void updateBorderRadiusPropValue( folly::dynamic& result, - const std::string& propName, + const char* propName, const std::optional& newValue, const std::optional& oldValue) { if (newValue != oldValue) { @@ -298,7 +298,7 @@ static void updateBorderStyleProps( static void updateBorderColorPropValue( folly::dynamic& result, - const std::string& propName, + const char* propName, const std::optional& newColor, const std::optional& oldColor) { if (newColor != oldColor) { @@ -344,7 +344,7 @@ static void updateBorderColorsProps( inline static void updateNativeDrawableProp( folly::dynamic& result, - const std::string& propName, + const char* propName, const std::optional& nativeDrawable) { folly::dynamic nativeDrawableResult; if (nativeDrawable.has_value()) { @@ -392,7 +392,7 @@ inline static void updateNativeDrawableProp( nativeDrawableResult = folly::dynamic(nullptr); } - result[propName] = nativeDrawableResult; + result[propName] = std::move(nativeDrawableResult); } static folly::dynamic toDynamic(const AccessibilityState& state) { @@ -872,8 +872,8 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } // Transforms - if (transform != oldProps->transform || - transformOrigin != oldProps->transformOrigin) { + // Java re-applies the last transform when only the origin changes. + if (transform != oldProps->transform) { folly::dynamic resultTranslateArray = folly::dynamic::array(); for (const auto& operation : transform.operations) { updateTransformProps(transform, operation, resultTranslateArray); @@ -903,7 +903,8 @@ folly::dynamic HostPlatformViewProps::getDiffProps( accessibilityLabelledBy.value) { accessibilityLabelledByValues.push_back(accessibilityLabelledByValue); } - result["accessibilityLabelledBy"] = accessibilityLabelledByValues; + result["accessibilityLabelledBy"] = + std::move(accessibilityLabelledByValues); } if (accessibilityOrder != oldProps->accessibilityOrder) { @@ -911,7 +912,8 @@ folly::dynamic HostPlatformViewProps::getDiffProps( for (const auto& accessibilityChildId : accessibilityOrder) { accessibilityChildrenIds.push_back(accessibilityChildId); } - result["experimental_accessibilityOrder"] = accessibilityChildrenIds; + result["experimental_accessibilityOrder"] = + std::move(accessibilityChildrenIds); } if (accessibilityLiveRegion != oldProps->accessibilityLiveRegion) { @@ -948,7 +950,7 @@ folly::dynamic HostPlatformViewProps::getDiffProps( } result["accessibilityValue"] = accessibilityValueObject.empty() ? folly::dynamic(nullptr) - : accessibilityValueObject; + : std::move(accessibilityValueObject); } if (accessibilityActions != oldProps->accessibilityActions) { @@ -959,9 +961,9 @@ folly::dynamic HostPlatformViewProps::getDiffProps( if (accessibilityAction.label.has_value()) { accessibilityActionObject["label"] = accessibilityAction.label.value(); } - accessibilityActionsArray.push_back(accessibilityActionObject); + accessibilityActionsArray.push_back(std::move(accessibilityActionObject)); } - result["accessibilityActions"] = accessibilityActionsArray; + result["accessibilityActions"] = std::move(accessibilityActionsArray); } if (accessibilityViewIsModal != oldProps->accessibilityViewIsModal) { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp index 7fe5a6dfdab3..186ebe6c8fd2 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/ViewPropsDiffTest.cpp @@ -152,6 +152,37 @@ TEST(ViewPropsDiffTest, removingAccessibilityValueEmitsNull) { }); } +// BaseViewManager re-applies the stored transform when only the origin changes. +TEST(ViewPropsDiffTest, changingOnlyTransformOriginDoesNotResendTransform) { + auto origin = [](float x) { + TransformOrigin transformOrigin; + transformOrigin.xy[0] = ValueUnit(x, UnitType::Point); + return transformOrigin; + }; + ViewProps oldProps; + oldProps.transform = Transform::Translate(10, 0, 0); + oldProps.transformOrigin = origin(1); + ViewProps newProps; + newProps.transform = oldProps.transform; + newProps.transformOrigin = origin(2); + + auto diff = newProps.getDiffProps(&oldProps); + + EXPECT_EQ(diff.count("transformOrigin"), 1); + EXPECT_EQ(diff.count("transform"), 0); +} + +TEST(ViewPropsDiffTest, emitsOnlyTheChangedBorderRadiusCorner) { + ViewProps oldProps; + ViewProps newProps; + newProps.borderRadii.bottomRight = ValueUnit(4, UnitType::Point); + + auto diff = newProps.getDiffProps(&oldProps); + + ASSERT_EQ(diff.size(), 1); + EXPECT_DOUBLE_EQ(diff["borderBottomRightRadius"].asDouble(), 4); +} + } // namespace facebook::react #endif