From 32c5b9712a33e877eae2823394a96aeeb8e5411b Mon Sep 17 00:00:00 2001 From: Bao Nguyen Date: Thu, 13 Aug 2026 17:02:28 +0700 Subject: [PATCH] fix: Re-apply all props when Fabric re-creates a HybridView's native View `CachedProp::isDirty` tracks whether a prop changed in the ShadowTree - not whether it has ever been applied to a specific native View. Fabric can create a new native View for a ShadowNode that did not change (for example when a subtree is hidden and shown again, which is what react-native-screens does when a screen is detached and re-attached). All props are clean at that point, so no setter runs and the fresh View keeps its Swift/Kotlin defaults. A newly created - or recycled - View now applies every prop it has once, which is what React Native core Views do implicitly by diffing against `oldProps` (`defaultProps` for a fresh View). Props that JS never set are skipped via the new `CachedProp::hasValue()`, so their default-constructed value is never pushed into the View. Steady-state updates are unaffected: after the first update the flag is set and only dirty props are applied, so the JNI/Swift roundtrip optimization from #1195 still holds. Fixes #1380 --- example/__tests__/views.harness.tsx | 51 +++++++++++++++++++ .../views/kotlin/KotlinHybridViewManager.ts | 30 ++++++++--- .../src/views/swift/SwiftHybridViewManager.ts | 20 ++++++-- .../android/src/main/res/values/ids.xml | 1 + .../cpp/views/CachedProp.hpp | 9 ++++ .../JHybridRecyclableTestViewStateUpdater.cpp | 7 +-- .../JHybridRecyclableTestViewStateUpdater.hpp | 3 +- .../c++/views/JHybridTestViewStateUpdater.cpp | 13 ++--- .../c++/views/JHybridTestViewStateUpdater.hpp | 3 +- .../views/HybridRecyclableTestViewManager.kt | 18 +++++-- .../HybridRecyclableTestViewStateUpdater.kt | 2 +- .../nitro/test/views/HybridTestViewManager.kt | 18 +++++-- .../test/views/HybridTestViewStateUpdater.kt | 2 +- .../HybridRecyclableTestViewComponent.mm | 20 ++++++-- .../ios/c++/views/HybridTestViewComponent.mm | 26 +++++++--- 15 files changed, 178 insertions(+), 45 deletions(-) create mode 100644 example/__tests__/views.harness.tsx diff --git a/example/__tests__/views.harness.tsx b/example/__tests__/views.harness.tsx new file mode 100644 index 0000000000..523cf66c4f --- /dev/null +++ b/example/__tests__/views.harness.tsx @@ -0,0 +1,51 @@ +import { Activity } from 'react' +import { Platform } from 'react-native' +import { describe, expect, it, render, waitUntil } from 'react-native-harness' +import { callback } from 'react-native-nitro-modules' +import { TestView, type TestViewRef } from 'react-native-nitro-test' + +const refs: TestViewRef[] = [] +const hybridRef = callback((ref: TestViewRef) => { + refs.push(ref) +}) + +// Stable element - React does not re-render it, so its ShadowNode never changes. +const testView = ( + {})} + hybridRef={hybridRef} + style={{ width: 20, height: 20 }} + /> +) +const renderTestView = (visible: boolean) => ( + {testView} +) + +// On iOS, hiding a subtree drops its native Views and showing it again creates new ones - for +// ShadowNodes that did not change. This is what react-native-screens does when a screen is +// detached and re-attached. On Android the native View survives being hidden, so there is +// nothing to re-create there. See https://github.com/mrousavy/nitro/issues/1380 +const itOnIos = Platform.OS === 'ios' ? it : it.skip + +describe('HybridView', () => { + itOnIos('applies all props to a native View that Fabric re-created', async () => { + refs.length = 0 + + const { rerender, unmount } = await render(renderTestView(true)) + await waitUntil(() => refs.length === 1) + expect(refs[0]!.isBlue).toBe(true) + + await rerender(renderTestView(false)) + await rerender(renderTestView(true)) + + // A new native View was created, so `hybridRef` fires again with the new HybridView... + await waitUntil(() => refs.length === 2) + // ...and that new HybridView received `isBlue` again, instead of keeping its native default. + expect(refs[1]!.isBlue).toBe(true) + + unmount() + }) +}) diff --git a/packages/nitrogen/src/views/kotlin/KotlinHybridViewManager.ts b/packages/nitrogen/src/views/kotlin/KotlinHybridViewManager.ts index a27718115b..971d8612c1 100644 --- a/packages/nitrogen/src/views/kotlin/KotlinHybridViewManager.ts +++ b/packages/nitrogen/src/views/kotlin/KotlinHybridViewManager.ts @@ -49,6 +49,7 @@ import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.R.id.associated_hybrid_view_tag +import com.margelo.nitro.R.id.needs_full_props_update_tag import com.margelo.nitro.views.RecyclableView import ${javaNamespace}.* @@ -71,6 +72,7 @@ public class ${manager}: SimpleViewManager() { val hybridView = ${viewImplementation}(reactContext) val view = hybridView.view view.setTag(associated_hybrid_view_tag, hybridView) + view.setTag(needs_full_props_update_tag, true) return view } @@ -78,12 +80,19 @@ public class ${manager}: SimpleViewManager() { val hybridView = getHybridView(view) ?: throw Error("Couldn't find view $view in local views table!") - // 1. Update each prop individually + // 1. \`isDirty\` only tells us whether a prop changed in the ShadowTree - not whether it has + // ever been applied to this View. Fabric can create a new View for a ShadowNode that did + // not change (e.g. when a subtree is hidden and shown again), in which case no prop would + // be dirty at all. A newly created (or recycled) View therefore applies all props once. + val forceUpdate = view.getTag(needs_full_props_update_tag) as? Boolean ?: true + view.setTag(needs_full_props_update_tag, false) + + // 2. Update each prop individually hybridView.beforeUpdate() - ${stateUpdaterName}.updateViewProps(hybridView, stateWrapper) + ${stateUpdaterName}.updateViewProps(hybridView, stateWrapper, forceUpdate) hybridView.afterUpdate() - // 2. Continue in base View props + // 3. Continue in base View props return super.updateState(view, props, stateWrapper) } @@ -103,6 +112,9 @@ public class ${manager}: SimpleViewManager() { // Recycle in it's implementation hybridView.prepareForRecycle() + // This View will be re-used for a different ShadowNode later on, so it needs all props again. + hybridView.view.setTag(needs_full_props_update_tag, true) + // Maybe update the view if it changed return hybridView.view } else { @@ -132,7 +144,7 @@ internal class ${stateUpdaterName} { */ @Suppress("KotlinJniMissingFunction") @JvmStatic - external fun updateViewProps(view: ${HybridTSpec}, state: StateWrapper) + external fun updateViewProps(view: ${HybridTSpec}, state: StateWrapper, forceUpdate: Boolean) } } `.trim() @@ -171,7 +183,8 @@ public: public: static void updateViewProps(jni::alias_ref /* class */, jni::alias_ref<${JHybridTSpec}::JavaPart> view, - jni::alias_ref stateWrapperInterface); + jni::alias_ref stateWrapperInterface, + jboolean forceUpdate); public: static void registerNatives() { @@ -193,7 +206,7 @@ public: const name = escapeCppName(p.name) const setter = p.getSetterName('other') return ` -if (props->${name}.isDirty) { +if ((forceUpdate && props->${name}.hasValue()) || props->${name}.isDirty) { hybridView->${setter}(props->${name}.value); props->${name}.isDirty = false; } @@ -214,7 +227,8 @@ using ConcreteStateData = react::ConcreteState<${stateClassName}>; void J${stateUpdaterName}::updateViewProps(jni::alias_ref /* class */, jni::alias_ref<${JHybridTSpec}::JavaPart> javaView, - jni::alias_ref stateWrapperInterface) { + jni::alias_ref stateWrapperInterface, + jboolean forceUpdate) { std::shared_ptr<${JHybridTSpec}> hybridView = javaView->get${JHybridTSpec}(); // Get concrete StateWrapperImpl from passed StateWrapper interface object @@ -237,7 +251,7 @@ void J${stateUpdaterName}::updateViewProps(jni::alias_ref /* class ${indent(propsUpdaterCalls.join('\n'), ' ')} // Update hybridRef if it changed - if (props->hybridRef.isDirty) { + if ((forceUpdate && props->hybridRef.hasValue()) || props->hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = props->hybridRef.value; if (maybeFunc.has_value()) { diff --git a/packages/nitrogen/src/views/swift/SwiftHybridViewManager.ts b/packages/nitrogen/src/views/swift/SwiftHybridViewManager.ts index 68c579ffb6..3c1eb74743 100644 --- a/packages/nitrogen/src/views/swift/SwiftHybridViewManager.ts +++ b/packages/nitrogen/src/views/swift/SwiftHybridViewManager.ts @@ -45,7 +45,7 @@ export function createSwiftHybridViewManager( ) return ` // ${p.jsSignature} -if (newViewProps.${name}.isDirty) { +if ((forceUpdate && newViewProps.${name}.hasValue()) || newViewProps.${name}.isDirty) { swiftPart.${setter}(${indent(parse, ' ')}); newViewProps.${name}.isDirty = false; } @@ -87,6 +87,7 @@ using namespace ${namespace}::views; @implementation ${component} { std::shared_ptr<${HybridTSpecSwift}> _hybridView; + BOOL _didUpdateProps; } + (void) load { @@ -126,15 +127,22 @@ using namespace ${namespace}::views; auto& newViewProps = const_cast<${propsClassName}&>(newViewPropsConst); ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart(); - // 2. Update each prop individually + // 2. \`isDirty\` only tells us whether a prop changed in the ShadowTree - not whether it has + // ever been applied to *this* View. Fabric can create a new View for a ShadowNode that + // did not change (e.g. when a subtree is hidden and shown again), in which case no prop + // would be dirty at all. A newly created (or recycled) View therefore applies all props once. + const bool forceUpdate = !_didUpdateProps; + _didUpdateProps = YES; + + // 3. Update each prop individually swiftPart.beforeUpdate(); ${indent(propAssignments.join('\n'), ' ')} swiftPart.afterUpdate(); - // 3. Update hybridRef if it changed - if (newViewProps.hybridRef.isDirty) { + // 4. Update hybridRef if it changed + if ((forceUpdate && newViewProps.hybridRef.hasValue()) || newViewProps.hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = newViewProps.hybridRef.value; if (maybeFunc.has_value()) { @@ -143,7 +151,7 @@ using namespace ${namespace}::views; newViewProps.hybridRef.isDirty = false; } - // 4. Continue in base class + // 5. Continue in base class [super updateProps:props oldProps:oldProps]; } @@ -153,6 +161,8 @@ using namespace ${namespace}::views; - (void)prepareForRecycle { [super prepareForRecycle]; + // This View will be re-used for a different ShadowNode later on, so it needs all props again. + _didUpdateProps = NO; ${swiftNamespace}::${HybridTSpecCxx}& swiftPart = _hybridView->getSwiftPart(); swiftPart.maybePrepareForRecycle(); } diff --git a/packages/react-native-nitro-modules/android/src/main/res/values/ids.xml b/packages/react-native-nitro-modules/android/src/main/res/values/ids.xml index 0ddf9b8cb2..a85a3e3671 100644 --- a/packages/react-native-nitro-modules/android/src/main/res/values/ids.xml +++ b/packages/react-native-nitro-modules/android/src/main/res/values/ids.xml @@ -1,4 +1,5 @@ + diff --git a/packages/react-native-nitro-modules/cpp/views/CachedProp.hpp b/packages/react-native-nitro-modules/cpp/views/CachedProp.hpp index 744324d70d..9558bf1a25 100644 --- a/packages/react-native-nitro-modules/cpp/views/CachedProp.hpp +++ b/packages/react-native-nitro-modules/cpp/views/CachedProp.hpp @@ -33,6 +33,15 @@ struct CachedProp { BorrowingReference jsiValue; public: + /** + * Whether this prop ever received a value from JS. + * A prop that was never set by JS still holds a default-constructed `value`, + * which must not be applied to the View. + */ + bool hasValue() const noexcept { + return jsiValue != nullptr; + } + bool equals(jsi::Runtime& runtime, const jsi::Value& other) const { if (jsiValue == nullptr) { return false; diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.cpp b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.cpp index cb4b7c3a8e..7b51a67144 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.cpp +++ b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.cpp @@ -17,7 +17,8 @@ using ConcreteStateData = react::ConcreteState; void JHybridRecyclableTestViewStateUpdater::updateViewProps(jni::alias_ref /* class */, jni::alias_ref javaView, - jni::alias_ref stateWrapperInterface) { + jni::alias_ref stateWrapperInterface, + jboolean forceUpdate) { std::shared_ptr hybridView = javaView->getJHybridRecyclableTestViewSpec(); // Get concrete StateWrapperImpl from passed StateWrapper interface object @@ -37,13 +38,13 @@ void JHybridRecyclableTestViewStateUpdater::updateViewProps(jni::alias_refisBlue.isDirty) { + if ((forceUpdate && props->isBlue.hasValue()) || props->isBlue.isDirty) { hybridView->setIsBlue(props->isBlue.value); props->isBlue.isDirty = false; } // Update hybridRef if it changed - if (props->hybridRef.isDirty) { + if ((forceUpdate && props->hybridRef.hasValue()) || props->hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = props->hybridRef.value; if (maybeFunc.has_value()) { diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.hpp b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.hpp index 0bd9385b38..3dd86aba6f 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.hpp +++ b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridRecyclableTestViewStateUpdater.hpp @@ -31,7 +31,8 @@ class JHybridRecyclableTestViewStateUpdater final: public jni::JavaClass /* class */, jni::alias_ref view, - jni::alias_ref stateWrapperInterface); + jni::alias_ref stateWrapperInterface, + jboolean forceUpdate); public: static void registerNatives() { diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.cpp b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.cpp index fb65848189..8af8b3d39b 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.cpp +++ b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.cpp @@ -17,7 +17,8 @@ using ConcreteStateData = react::ConcreteState; void JHybridTestViewStateUpdater::updateViewProps(jni::alias_ref /* class */, jni::alias_ref javaView, - jni::alias_ref stateWrapperInterface) { + jni::alias_ref stateWrapperInterface, + jboolean forceUpdate) { std::shared_ptr hybridView = javaView->getJHybridTestViewSpec(); // Get concrete StateWrapperImpl from passed StateWrapper interface object @@ -37,25 +38,25 @@ void JHybridTestViewStateUpdater::updateViewProps(jni::alias_ref /* } // Update all props if they are dirty - if (props->isBlue.isDirty) { + if ((forceUpdate && props->isBlue.hasValue()) || props->isBlue.isDirty) { hybridView->setIsBlue(props->isBlue.value); props->isBlue.isDirty = false; } - if (props->hasBeenCalled.isDirty) { + if ((forceUpdate && props->hasBeenCalled.hasValue()) || props->hasBeenCalled.isDirty) { hybridView->setHasBeenCalled(props->hasBeenCalled.value); props->hasBeenCalled.isDirty = false; } - if (props->colorScheme.isDirty) { + if ((forceUpdate && props->colorScheme.hasValue()) || props->colorScheme.isDirty) { hybridView->setColorScheme(props->colorScheme.value); props->colorScheme.isDirty = false; } - if (props->someCallback.isDirty) { + if ((forceUpdate && props->someCallback.hasValue()) || props->someCallback.isDirty) { hybridView->setSomeCallback(props->someCallback.value); props->someCallback.isDirty = false; } // Update hybridRef if it changed - if (props->hybridRef.isDirty) { + if ((forceUpdate && props->hybridRef.hasValue()) || props->hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = props->hybridRef.value; if (maybeFunc.has_value()) { diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.hpp b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.hpp index b71ecbeb1f..ec3e9ed5e3 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.hpp +++ b/packages/react-native-nitro-test/nitrogen/generated/android/c++/views/JHybridTestViewStateUpdater.hpp @@ -31,7 +31,8 @@ class JHybridTestViewStateUpdater final: public jni::JavaClass /* class */, jni::alias_ref view, - jni::alias_ref stateWrapperInterface); + jni::alias_ref stateWrapperInterface, + jboolean forceUpdate); public: static void registerNatives() { diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewManager.kt b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewManager.kt index c325e51a57..ee6c552ad5 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewManager.kt +++ b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewManager.kt @@ -13,6 +13,7 @@ import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.R.id.associated_hybrid_view_tag +import com.margelo.nitro.R.id.needs_full_props_update_tag import com.margelo.nitro.views.RecyclableView import com.margelo.nitro.test.* @@ -35,6 +36,7 @@ public class HybridRecyclableTestViewManager: SimpleViewManager() { val hybridView = HybridRecyclableTestView(reactContext) val view = hybridView.view view.setTag(associated_hybrid_view_tag, hybridView) + view.setTag(needs_full_props_update_tag, true) return view } @@ -42,12 +44,19 @@ public class HybridRecyclableTestViewManager: SimpleViewManager() { val hybridView = getHybridView(view) ?: throw Error("Couldn't find view $view in local views table!") - // 1. Update each prop individually + // 1. `isDirty` only tells us whether a prop changed in the ShadowTree - not whether it has + // ever been applied to this View. Fabric can create a new View for a ShadowNode that did + // not change (e.g. when a subtree is hidden and shown again), in which case no prop would + // be dirty at all. A newly created (or recycled) View therefore applies all props once. + val forceUpdate = view.getTag(needs_full_props_update_tag) as? Boolean ?: true + view.setTag(needs_full_props_update_tag, false) + + // 2. Update each prop individually hybridView.beforeUpdate() - HybridRecyclableTestViewStateUpdater.updateViewProps(hybridView, stateWrapper) + HybridRecyclableTestViewStateUpdater.updateViewProps(hybridView, stateWrapper, forceUpdate) hybridView.afterUpdate() - // 2. Continue in base View props + // 3. Continue in base View props return super.updateState(view, props, stateWrapper) } @@ -67,6 +76,9 @@ public class HybridRecyclableTestViewManager: SimpleViewManager() { // Recycle in it's implementation hybridView.prepareForRecycle() + // This View will be re-used for a different ShadowNode later on, so it needs all props again. + hybridView.view.setTag(needs_full_props_update_tag, true) + // Maybe update the view if it changed return hybridView.view } else { diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewStateUpdater.kt b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewStateUpdater.kt index ce1fd30101..2cabca46a2 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewStateUpdater.kt +++ b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridRecyclableTestViewStateUpdater.kt @@ -18,6 +18,6 @@ internal class HybridRecyclableTestViewStateUpdater { */ @Suppress("KotlinJniMissingFunction") @JvmStatic - external fun updateViewProps(view: HybridRecyclableTestViewSpec, state: StateWrapper) + external fun updateViewProps(view: HybridRecyclableTestViewSpec, state: StateWrapper, forceUpdate: Boolean) } } diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewManager.kt b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewManager.kt index 6ee9d802a2..32014f715b 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewManager.kt +++ b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewManager.kt @@ -13,6 +13,7 @@ import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.StateWrapper import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.R.id.associated_hybrid_view_tag +import com.margelo.nitro.R.id.needs_full_props_update_tag import com.margelo.nitro.views.RecyclableView import com.margelo.nitro.test.* @@ -35,6 +36,7 @@ public class HybridTestViewManager: SimpleViewManager() { val hybridView = HybridTestView(reactContext) val view = hybridView.view view.setTag(associated_hybrid_view_tag, hybridView) + view.setTag(needs_full_props_update_tag, true) return view } @@ -42,12 +44,19 @@ public class HybridTestViewManager: SimpleViewManager() { val hybridView = getHybridView(view) ?: throw Error("Couldn't find view $view in local views table!") - // 1. Update each prop individually + // 1. `isDirty` only tells us whether a prop changed in the ShadowTree - not whether it has + // ever been applied to this View. Fabric can create a new View for a ShadowNode that did + // not change (e.g. when a subtree is hidden and shown again), in which case no prop would + // be dirty at all. A newly created (or recycled) View therefore applies all props once. + val forceUpdate = view.getTag(needs_full_props_update_tag) as? Boolean ?: true + view.setTag(needs_full_props_update_tag, false) + + // 2. Update each prop individually hybridView.beforeUpdate() - HybridTestViewStateUpdater.updateViewProps(hybridView, stateWrapper) + HybridTestViewStateUpdater.updateViewProps(hybridView, stateWrapper, forceUpdate) hybridView.afterUpdate() - // 2. Continue in base View props + // 3. Continue in base View props return super.updateState(view, props, stateWrapper) } @@ -67,6 +76,9 @@ public class HybridTestViewManager: SimpleViewManager() { // Recycle in it's implementation hybridView.prepareForRecycle() + // This View will be re-used for a different ShadowNode later on, so it needs all props again. + hybridView.view.setTag(needs_full_props_update_tag, true) + // Maybe update the view if it changed return hybridView.view } else { diff --git a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewStateUpdater.kt b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewStateUpdater.kt index 7c5c8d25c1..e6865fda35 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewStateUpdater.kt +++ b/packages/react-native-nitro-test/nitrogen/generated/android/kotlin/com/margelo/nitro/test/views/HybridTestViewStateUpdater.kt @@ -18,6 +18,6 @@ internal class HybridTestViewStateUpdater { */ @Suppress("KotlinJniMissingFunction") @JvmStatic - external fun updateViewProps(view: HybridTestViewSpec, state: StateWrapper) + external fun updateViewProps(view: HybridTestViewSpec, state: StateWrapper, forceUpdate: Boolean) } } diff --git a/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridRecyclableTestViewComponent.mm b/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridRecyclableTestViewComponent.mm index 06ecc420d1..0a925ad038 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridRecyclableTestViewComponent.mm +++ b/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridRecyclableTestViewComponent.mm @@ -37,6 +37,7 @@ + (BOOL)shouldBeRecycled; @implementation HybridRecyclableTestViewComponent { std::shared_ptr _hybridView; + BOOL _didUpdateProps; } + (void) load { @@ -76,19 +77,26 @@ - (void) updateProps:(const std::shared_ptr&)props auto& newViewProps = const_cast(newViewPropsConst); NitroTest::HybridRecyclableTestViewSpec_cxx& swiftPart = _hybridView->getSwiftPart(); - // 2. Update each prop individually + // 2. `isDirty` only tells us whether a prop changed in the ShadowTree - not whether it has + // ever been applied to *this* View. Fabric can create a new View for a ShadowNode that + // did not change (e.g. when a subtree is hidden and shown again), in which case no prop + // would be dirty at all. A newly created (or recycled) View therefore applies all props once. + const bool forceUpdate = !_didUpdateProps; + _didUpdateProps = YES; + + // 3. Update each prop individually swiftPart.beforeUpdate(); // isBlue: boolean - if (newViewProps.isBlue.isDirty) { + if ((forceUpdate && newViewProps.isBlue.hasValue()) || newViewProps.isBlue.isDirty) { swiftPart.setIsBlue(newViewProps.isBlue.value); newViewProps.isBlue.isDirty = false; } swiftPart.afterUpdate(); - // 3. Update hybridRef if it changed - if (newViewProps.hybridRef.isDirty) { + // 4. Update hybridRef if it changed + if ((forceUpdate && newViewProps.hybridRef.hasValue()) || newViewProps.hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = newViewProps.hybridRef.value; if (maybeFunc.has_value()) { @@ -97,7 +105,7 @@ - (void) updateProps:(const std::shared_ptr&)props newViewProps.hybridRef.isDirty = false; } - // 4. Continue in base class + // 5. Continue in base class [super updateProps:props oldProps:oldProps]; } @@ -107,6 +115,8 @@ + (BOOL)shouldBeRecycled { - (void)prepareForRecycle { [super prepareForRecycle]; + // This View will be re-used for a different ShadowNode later on, so it needs all props again. + _didUpdateProps = NO; NitroTest::HybridRecyclableTestViewSpec_cxx& swiftPart = _hybridView->getSwiftPart(); swiftPart.maybePrepareForRecycle(); } diff --git a/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridTestViewComponent.mm b/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridTestViewComponent.mm index 191b76da7a..f8b72c0744 100644 --- a/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridTestViewComponent.mm +++ b/packages/react-native-nitro-test/nitrogen/generated/ios/c++/views/HybridTestViewComponent.mm @@ -37,6 +37,7 @@ + (BOOL)shouldBeRecycled; @implementation HybridTestViewComponent { std::shared_ptr _hybridView; + BOOL _didUpdateProps; } + (void) load { @@ -76,34 +77,41 @@ - (void) updateProps:(const std::shared_ptr&)props auto& newViewProps = const_cast(newViewPropsConst); NitroTest::HybridTestViewSpec_cxx& swiftPart = _hybridView->getSwiftPart(); - // 2. Update each prop individually + // 2. `isDirty` only tells us whether a prop changed in the ShadowTree - not whether it has + // ever been applied to *this* View. Fabric can create a new View for a ShadowNode that + // did not change (e.g. when a subtree is hidden and shown again), in which case no prop + // would be dirty at all. A newly created (or recycled) View therefore applies all props once. + const bool forceUpdate = !_didUpdateProps; + _didUpdateProps = YES; + + // 3. Update each prop individually swiftPart.beforeUpdate(); // isBlue: boolean - if (newViewProps.isBlue.isDirty) { + if ((forceUpdate && newViewProps.isBlue.hasValue()) || newViewProps.isBlue.isDirty) { swiftPart.setIsBlue(newViewProps.isBlue.value); newViewProps.isBlue.isDirty = false; } // hasBeenCalled: boolean - if (newViewProps.hasBeenCalled.isDirty) { + if ((forceUpdate && newViewProps.hasBeenCalled.hasValue()) || newViewProps.hasBeenCalled.isDirty) { swiftPart.setHasBeenCalled(newViewProps.hasBeenCalled.value); newViewProps.hasBeenCalled.isDirty = false; } // colorScheme: enum - if (newViewProps.colorScheme.isDirty) { + if ((forceUpdate && newViewProps.colorScheme.hasValue()) || newViewProps.colorScheme.isDirty) { swiftPart.setColorScheme(static_cast(newViewProps.colorScheme.value)); newViewProps.colorScheme.isDirty = false; } // someCallback: function - if (newViewProps.someCallback.isDirty) { + if ((forceUpdate && newViewProps.someCallback.hasValue()) || newViewProps.someCallback.isDirty) { swiftPart.setSomeCallback(newViewProps.someCallback.value); newViewProps.someCallback.isDirty = false; } swiftPart.afterUpdate(); - // 3. Update hybridRef if it changed - if (newViewProps.hybridRef.isDirty) { + // 4. Update hybridRef if it changed + if ((forceUpdate && newViewProps.hybridRef.hasValue()) || newViewProps.hybridRef.isDirty) { // hybridRef changed - call it with new this const auto& maybeFunc = newViewProps.hybridRef.value; if (maybeFunc.has_value()) { @@ -112,7 +120,7 @@ - (void) updateProps:(const std::shared_ptr&)props newViewProps.hybridRef.isDirty = false; } - // 4. Continue in base class + // 5. Continue in base class [super updateProps:props oldProps:oldProps]; } @@ -122,6 +130,8 @@ + (BOOL)shouldBeRecycled { - (void)prepareForRecycle { [super prepareForRecycle]; + // This View will be re-used for a different ShadowNode later on, so it needs all props again. + _didUpdateProps = NO; NitroTest::HybridTestViewSpec_cxx& swiftPart = _hybridView->getSwiftPart(); swiftPart.maybePrepareForRecycle(); }