From b03e255ac09b6e4cd1ffb5fa47dd1d61fc37aa5e Mon Sep 17 00:00:00 2001 From: sudoplz Date: Wed, 29 Apr 2026 02:13:47 +0300 Subject: [PATCH] Add useTraitHiddenOnIOS feature flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Mirrors useTraitHiddenOnAndroid from #54112. Lets iOS apps opt out of the `Trait::Hidden` slice-skip; Android can already opt in. Default is `true`, preserving today's iOS behavior. What the optimization does D22134220 (2020, "Fabric: display: none nodes do not create views anymore") added `Trait::Hidden` and a slice-skip in `sliceChildShadowNodeViewPairs` that filters out subtrees whose Yoga display is None. On iOS the diff then issues REMOVE + DELETE for the entire subtree — invisible content stops costing anything. For most UIs this is a clean win. Where it bites The optimization assumes that `display: none` transitions are rare. That breaks for a specific pattern: **a custom host component mounted under ``**. Each suspend → resume cycle, Suspense flips display between `flex` and `none`. With the optimization on, every cycle: - tears down the entire subtree of UIViews, - drops per-instance native state — measurement caches, scroll position, animation drivers, anything internal to the host component, - re-runs `init`/`dealloc` and rebuilds the subtree on resume. Suspend/resume ends up heavier than a fresh mount, and any state the component held disappears between renders. What the flag does **It re-activates an existing code path. Nothing in the mounting layer is new.** The hide-via-`UIView.hidden` wiring shipped in D8460108 (June 2018, "Fabric: Default support of displayType and layoutDirection layout...") and has lived in `UIView+ComponentViewProtocol updateLayoutMetrics:` ever since — a few lines below the slice consumer in the same file. For two years it was the only iOS path; the 2020 slice-skip didn't replace it, it just made it unreachable in the common case. Setting the flag to `false` lets Hidden shadow nodes pass through the slice. The differ emits an `UPDATE_LAYOUT_METRICS` mutation with `displayType == None`, and the 2018 wiring picks it up: `self.hidden = YES` on the underlying UIView. Same view, hidden in place. Defaults - `useTraitHiddenOnIOS = true` — keeps the iOS behavior introduced in 2020. - `useTraitHiddenOnAndroid = false` — keeps the Android behavior, which never adopted the slice-skip. Both flags share the same semantic ("use the optimization"); the defaults encode each platform's pre-flag behavior. Flipping either default is out of scope. ## Changelog: [IOS] [ADDED] - useTraitHiddenOnIOS feature flag to opt out of the `display: none` slice-skip optimization ## Test Plan: No new tests. `StackingContextTest` exercises the slice-skip path and passes unchanged with the flag at its default `true`. Manually flipping the flag to `false` produces the existing Android branch's expected view tree (8 views, Hidden subtrees preserved with `self.hidden = YES`). --- .../featureflags/ReactNativeFeatureFlags.kt | 8 +++++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +++++++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 ++- .../ReactNativeFeatureFlagsDefaults.kt | 4 ++- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 ++++++++- .../ReactNativeFeatureFlagsProvider.kt | 4 ++- .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++++++++++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +++- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +++- .../featureflags/ReactNativeFeatureFlags.h | 7 ++++- .../ReactNativeFeatureFlagsAccessor.cpp | 28 +++++++++++++++---- .../ReactNativeFeatureFlagsAccessor.h | 6 ++-- .../ReactNativeFeatureFlagsDefaults.h | 6 +++- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +++++++- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 ++++- .../NativeReactNativeFeatureFlags.h | 4 ++- .../sliceChildShadowNodeViewPairs.cpp | 6 +++- .../ReactNativeFeatureFlags.config.js | 11 ++++++++ .../featureflags/ReactNativeFeatureFlags.js | 7 ++++- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 21 files changed, 146 insertions(+), 25 deletions(-) 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 31377d183c2a..18b18c8bffc6 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<<94a1d6bbc5595c495a2c54851a9c5b5b>> + * @generated SignedSource<> */ /** @@ -396,6 +396,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun useSilenceErrorSMMViewNotFound(): Boolean = accessor.useSilenceErrorSMMViewNotFound() + /** + * iOS only. When true (default), shadow nodes carrying ShadowNodeTraits::Trait::Hidden are filtered out of the mounting slice. When false, those nodes stay in the slice and are hidden via UIView.hidden = YES in updateLayoutMetrics:. + */ + @JvmStatic + public fun useTraitHiddenOnIOS(): Boolean = accessor.useTraitHiddenOnIOS() + /** * In Bridgeless mode, should legacy NativeModules use the TurboModule system? */ 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 29369720feee..c814df9eab66 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<<4e19cdce9383c287c849b912fa74586c>> + * @generated SignedSource<> */ /** @@ -81,6 +81,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var useRawPropsJsiValueCache: Boolean? = null private var useShadowNodeStateOnCloneCache: Boolean? = null private var useSilenceErrorSMMViewNotFoundCache: Boolean? = null + private var useTraitHiddenOnIOSCache: Boolean? = null private var useTurboModuleInteropCache: Boolean? = null private var useTurboModulesCache: Boolean? = null private var virtualViewPrerenderRatioCache: Double? = null @@ -635,6 +636,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun useTraitHiddenOnIOS(): Boolean { + var cached = useTraitHiddenOnIOSCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.useTraitHiddenOnIOS() + useTraitHiddenOnIOSCache = cached + } + return cached + } + override fun useTurboModuleInterop(): Boolean { var cached = useTurboModuleInteropCache 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 6944d67d469b..abdef81d36f0 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<<2a525ef08b04eb1c244badb8cc865e4d>> + * @generated SignedSource<<1234d5c7c6bb64451618817387318d13>> */ /** @@ -150,6 +150,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun useSilenceErrorSMMViewNotFound(): Boolean + @DoNotStrip @JvmStatic public external fun useTraitHiddenOnIOS(): Boolean + @DoNotStrip @JvmStatic public external fun useTurboModuleInterop(): Boolean @DoNotStrip @JvmStatic public external fun useTurboModules(): 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 07e9c9a9a359..12c9c2d2a564 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<<58b3101bd16b63268440523cdee7ae93>> + * @generated SignedSource<<3255db6cb7c71d866991568dc632321f>> */ /** @@ -145,6 +145,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun useSilenceErrorSMMViewNotFound(): Boolean = false + override fun useTraitHiddenOnIOS(): Boolean = true + override fun useTurboModuleInterop(): Boolean = false override fun useTurboModules(): Boolean = false 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 e7cfc279c115..6058c9cd3dfd 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<<3c639d5edf52fc3feee65388b5ac3ed7>> + * @generated SignedSource<<6f23f8fd399adf79814bb8f2edc29841>> */ /** @@ -85,6 +85,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var useRawPropsJsiValueCache: Boolean? = null private var useShadowNodeStateOnCloneCache: Boolean? = null private var useSilenceErrorSMMViewNotFoundCache: Boolean? = null + private var useTraitHiddenOnIOSCache: Boolean? = null private var useTurboModuleInteropCache: Boolean? = null private var useTurboModulesCache: Boolean? = null private var virtualViewPrerenderRatioCache: Double? = null @@ -700,6 +701,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun useTraitHiddenOnIOS(): Boolean { + var cached = useTraitHiddenOnIOSCache + if (cached == null) { + cached = currentProvider.useTraitHiddenOnIOS() + accessedFeatureFlags.add("useTraitHiddenOnIOS") + useTraitHiddenOnIOSCache = cached + } + return cached + } + override fun useTurboModuleInterop(): Boolean { var cached = useTurboModuleInteropCache if (cached == null) { 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 4d178df1e695..35726811606f 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<<0a4b05562ed8b30b7d4dabd5c8680d6d>> + * @generated SignedSource<<2877a4acf03951884d1d663fdf00b9f8>> */ /** @@ -145,6 +145,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun useSilenceErrorSMMViewNotFound(): Boolean + @DoNotStrip public fun useTraitHiddenOnIOS(): Boolean + @DoNotStrip public fun useTurboModuleInterop(): Boolean @DoNotStrip public fun useTurboModules(): Boolean 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 8d099d054216..479d7f71c528 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<<64b1d9667e6869ea4ba406222b307464>> + * @generated SignedSource<> */ /** @@ -405,6 +405,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool useTraitHiddenOnIOS() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useTraitHiddenOnIOS"); + return method(javaProvider_); + } + bool useTurboModuleInterop() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("useTurboModuleInterop"); @@ -738,6 +744,11 @@ bool JReactNativeFeatureFlagsCxxInterop::useSilenceErrorSMMViewNotFound( return ReactNativeFeatureFlags::useSilenceErrorSMMViewNotFound(); } +bool JReactNativeFeatureFlagsCxxInterop::useTraitHiddenOnIOS( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::useTraitHiddenOnIOS(); +} + bool JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::useTurboModuleInterop(); @@ -972,6 +983,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "useSilenceErrorSMMViewNotFound", JReactNativeFeatureFlagsCxxInterop::useSilenceErrorSMMViewNotFound), + makeNativeMethod( + "useTraitHiddenOnIOS", + JReactNativeFeatureFlagsCxxInterop::useTraitHiddenOnIOS), makeNativeMethod( "useTurboModuleInterop", JReactNativeFeatureFlagsCxxInterop::useTurboModuleInterop), 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 6e10979d4de3..10457edc1088 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<<073595986845439fdcb30edc52dadc30>> + * @generated SignedSource<> */ /** @@ -213,6 +213,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool useSilenceErrorSMMViewNotFound( facebook::jni::alias_ref); + static bool useTraitHiddenOnIOS( + facebook::jni::alias_ref); + static bool useTurboModuleInterop( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 2d92c8b3cb39..123729aac840 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<> + * @generated SignedSource<> */ /** @@ -270,6 +270,10 @@ bool ReactNativeFeatureFlags::useSilenceErrorSMMViewNotFound() { return getAccessor().useSilenceErrorSMMViewNotFound(); } +bool ReactNativeFeatureFlags::useTraitHiddenOnIOS() { + return getAccessor().useTraitHiddenOnIOS(); +} + bool ReactNativeFeatureFlags::useTurboModuleInterop() { return getAccessor().useTurboModuleInterop(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 1709aa4ab340..63855748befe 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<<7b1258f9970366f0c068f28e2c16ce12>> + * @generated SignedSource<<8dc801da5ad5fd085dcedb245f7dee08>> */ /** @@ -344,6 +344,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool useSilenceErrorSMMViewNotFound(); + /** + * iOS only. When true (default), shadow nodes carrying ShadowNodeTraits::Trait::Hidden are filtered out of the mounting slice. When false, those nodes stay in the slice and are hidden via UIView.hidden = YES in updateLayoutMetrics:. + */ + RN_EXPORT static bool useTraitHiddenOnIOS(); + /** * In Bridgeless mode, should legacy NativeModules use the TurboModule system? */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 8cf6d95dbb76..6e90d149e633 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<<85d9a99e79df5428b4b6264cedbc505c>> + * @generated SignedSource<<10aa043b91c4189bd7772066085e168d>> */ /** @@ -1127,6 +1127,24 @@ bool ReactNativeFeatureFlagsAccessor::useSilenceErrorSMMViewNotFound() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnIOS() { + auto flagValue = useTraitHiddenOnIOS_.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(61, "useTraitHiddenOnIOS"); + + flagValue = currentProvider_->useTraitHiddenOnIOS(); + useTraitHiddenOnIOS_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { auto flagValue = useTurboModuleInterop_.load(); @@ -1136,7 +1154,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(61, "useTurboModuleInterop"); + markFlagAsAccessed(62, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModules() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "useTurboModules"); + markFlagAsAccessed(63, "useTurboModules"); flagValue = currentProvider_->useTurboModules(); useTurboModules_ = flagValue; @@ -1172,7 +1190,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(63, "virtualViewPrerenderRatio"); + markFlagAsAccessed(64, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::runtimeCrashUiThreadUtils() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "runtimeCrashUiThreadUtils"); + markFlagAsAccessed(65, "runtimeCrashUiThreadUtils"); flagValue = currentProvider_->runtimeCrashUiThreadUtils(); runtimeCrashUiThreadUtils_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 84dedd0d1131..860aea749315 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<<8abeb54955dff119219c983bca1024e9>> + * @generated SignedSource<<98d2d767bf370439a9efc44a745ae802>> */ /** @@ -93,6 +93,7 @@ class ReactNativeFeatureFlagsAccessor { bool useRawPropsJsiValue(); bool useShadowNodeStateOnClone(); bool useSilenceErrorSMMViewNotFound(); + bool useTraitHiddenOnIOS(); bool useTurboModuleInterop(); bool useTurboModules(); double virtualViewPrerenderRatio(); @@ -108,7 +109,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 65> accessedFeatureFlags_; + std::array, 66> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> animatedShouldSignalBatch_; @@ -171,6 +172,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> useRawPropsJsiValue_; std::atomic> useShadowNodeStateOnClone_; std::atomic> useSilenceErrorSMMViewNotFound_; + std::atomic> useTraitHiddenOnIOS_; std::atomic> useTurboModuleInterop_; std::atomic> useTurboModules_; std::atomic> virtualViewPrerenderRatio_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 1b74ec9ae605..873efaebfcb2 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<<19c983e30b5131ab5904437f4cc9bbb4>> + * @generated SignedSource<<5cb395cf45a846c4e93b4eb5ca2d99e3>> */ /** @@ -271,6 +271,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool useTraitHiddenOnIOS() override { + return true; + } + bool useTurboModuleInterop() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 8f4cbe59a6d3..9566d80657b3 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<<52f0e78c7c4e53f8adac6cc796a79d7c>> + * @generated SignedSource<<627e0e191716b63f3a5dd7fd343506b9>> */ /** @@ -594,6 +594,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::useSilenceErrorSMMViewNotFound(); } + bool useTraitHiddenOnIOS() override { + auto value = values_["useTraitHiddenOnIOS"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::useTraitHiddenOnIOS(); + } + bool useTurboModuleInterop() override { auto value = values_["useTurboModuleInterop"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 80e588439fc0..8c6d60c9646c 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<<6001012c023fa00b026359cb87fbf2b2>> + * @generated SignedSource<> */ /** @@ -86,6 +86,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool useRawPropsJsiValue() = 0; virtual bool useShadowNodeStateOnClone() = 0; virtual bool useSilenceErrorSMMViewNotFound() = 0; + virtual bool useTraitHiddenOnIOS() = 0; virtual bool useTurboModuleInterop() = 0; virtual bool useTurboModules() = 0; virtual double virtualViewPrerenderRatio() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 55fa3ef168d1..8f3742fca812 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<<0b28e6659211377d63a87f71cf74f4c9>> + * @generated SignedSource<<4f37b3bec59e6d9cd6e61e4ecbbe7b6d>> */ /** @@ -349,6 +349,11 @@ bool NativeReactNativeFeatureFlags::useSilenceErrorSMMViewNotFound( return ReactNativeFeatureFlags::useSilenceErrorSMMViewNotFound(); } +bool NativeReactNativeFeatureFlags::useTraitHiddenOnIOS( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::useTraitHiddenOnIOS(); +} + bool NativeReactNativeFeatureFlags::useTurboModuleInterop( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::useTurboModuleInterop(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7aa7bd1f96a8..9218683bd9b8 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<<5595823614f9ec3f20d95ce21cb7b4d2>> + * @generated SignedSource<<267e09d240d84ae5613aa58e2ed1deb8>> */ /** @@ -158,6 +158,8 @@ class NativeReactNativeFeatureFlags bool useSilenceErrorSMMViewNotFound(jsi::Runtime& runtime); + bool useTraitHiddenOnIOS(jsi::Runtime& runtime); + bool useTurboModuleInterop(jsi::Runtime& runtime); bool useTurboModules(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp index 024a76689ae9..cda18e4121c6 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/internal/sliceChildShadowNodeViewPairs.cpp @@ -59,7 +59,11 @@ static void sliceChildShadowNodeViewPairsRecursively( #ifndef ANDROID // T153547836: Disabled on Android because the mounting infrastructure // is not fully ready yet. - if (childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) { + // On iOS, gated by useTraitHiddenOnIOS. When false, the view stays in + // the slice and is hidden via UIView.hidden = YES in + // updateLayoutMetrics: instead of being removed. + if (ReactNativeFeatureFlags::useTraitHiddenOnIOS() && + childShadowNode.getTraits().check(ShadowNodeTraits::Trait::Hidden)) { continue; } #endif diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index bf0b3f036498..924e620aadb2 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -692,6 +692,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + useTraitHiddenOnIOS: { + defaultValue: true, + metadata: { + dateAdded: '2026-04-29', + description: + 'iOS only. When true (default), shadow nodes carrying ShadowNodeTraits::Trait::Hidden are filtered out of the mounting slice. When false, those nodes stay in the slice and are hidden via UIView.hidden = YES in updateLayoutMetrics:.', + expectedReleaseValue: false, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, useTurboModuleInterop: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 3a676a2d7a7d..c1bf13b3bc58 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<<94e87435c32d010d0a3943fd71fc099f>> + * @generated SignedSource<<2d214619ebe2d8c61230de56ebfd95f6>> * @flow strict * @noformat */ @@ -112,6 +112,7 @@ export type ReactNativeFeatureFlags = $ReadOnly<{ useRawPropsJsiValue: Getter, useShadowNodeStateOnClone: Getter, useSilenceErrorSMMViewNotFound: Getter, + useTraitHiddenOnIOS: Getter, useTurboModuleInterop: Getter, useTurboModules: Getter, virtualViewPrerenderRatio: Getter, @@ -446,6 +447,10 @@ export const useShadowNodeStateOnClone: Getter = createNativeFlagGetter * Don't hard crash in SurfaceMountingManager when a view is not found. Instead, log a soft error. */ export const useSilenceErrorSMMViewNotFound: Getter = createNativeFlagGetter('useSilenceErrorSMMViewNotFound', false); +/** + * iOS only. When true (default), shadow nodes carrying ShadowNodeTraits::Trait::Hidden are filtered out of the mounting slice. When false, those nodes stay in the slice and are hidden via UIView.hidden = YES in updateLayoutMetrics:. + */ +export const useTraitHiddenOnIOS: Getter = createNativeFlagGetter('useTraitHiddenOnIOS', true); /** * In Bridgeless mode, should legacy NativeModules use the TurboModule system? */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index d74bf96732b0..c6d69e5004b5 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<> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -86,6 +86,7 @@ export interface Spec extends TurboModule { +useRawPropsJsiValue?: () => boolean; +useShadowNodeStateOnClone?: () => boolean; +useSilenceErrorSMMViewNotFound?: () => boolean; + +useTraitHiddenOnIOS?: () => boolean; +useTurboModuleInterop?: () => boolean; +useTurboModules?: () => boolean; +virtualViewPrerenderRatio?: () => number;