diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp index 4ff280cf23ee..20224379e120 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp @@ -126,6 +126,11 @@ ShadowNode::ShadowNode( react_native_assert(children_); traits_.set(ShadowNodeTraits::Trait::ChildrenAreShared); + // A clone that keeps the source's props keeps the native props they include. + if (fragment.props == ShadowNodeFragment::propsPlaceholder()) { + nativePropsVersion_ = sourceShadowNode.nativePropsVersion_; + } + if (fragment.children) { for (const auto& child : *children_) { child->family_->setParent(family_); @@ -140,11 +145,17 @@ std::shared_ptr ShadowNode::clone( const auto& componentDescriptor = family.componentDescriptor_; std::optional propsOverride; + uint32_t nativePropsVersion = 0; { std::lock_guard lock(family.nativePropsMutex); if (family.nativeProps_DEPRECATED != nullptr) { if (fragment.props == ShadowNodeFragment::propsPlaceholder()) { - propsOverride.emplace(*family.nativeProps_DEPRECATED); + // Props of a node that already include the current + // `nativeProps_DEPRECATED` don't need them applied again. + if (nativePropsVersion_ != family.nativePropsVersion_DEPRECATED) { + propsOverride.emplace(*family.nativeProps_DEPRECATED); + nativePropsVersion = family.nativePropsVersion_DEPRECATED; + } } else { // TODO: We might need to merge fragment.props with // `family.nativeProps_DEPRECATED`. @@ -158,12 +169,14 @@ std::shared_ptr ShadowNode::clone( // result escapes the lock so the rest of the clone work runs // unblocked. auto propsParserContext = PropsParserContext{family_->getSurfaceId(), {}}; - return componentDescriptor.cloneShadowNode( + auto clonedShadowNode = componentDescriptor.cloneShadowNode( *this, {.props = componentDescriptor.cloneProps( propsParserContext, props_, std::move(*propsOverride)), .children = fragment.children, .state = fragment.state}); + clonedShadowNode->nativePropsVersion_ = nativePropsVersion; + return clonedShadowNode; } return componentDescriptor.cloneShadowNode(*this, fragment); diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h index b21cb21b4f3b..7418a1e564bf 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNode.h @@ -282,6 +282,12 @@ class ShadowNode : public Sealable, public DebugStringConvertible, public jsi::N */ mutable std::atomic hasBeenMounted_{false}; + /* + * The family's `nativePropsVersion_DEPRECATED` that `props_` already + * include, so that cloning doesn't apply `nativeProps_DEPRECATED` again. + */ + uint32_t nativePropsVersion_{0}; + static Props::Shared propsForClonedShadowNode(const ShadowNode &sourceShadowNode, const Props::Shared &props); protected: diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h index 2ffc2ce5ac2a..8a9ef564d596 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeFamily.h @@ -130,6 +130,12 @@ class ShadowNodeFamily final : public jsi::NativeState { mutable std::unique_ptr nativeProps_DEPRECATED; mutable std::mutex nativePropsMutex; + /* + * Incremented whenever `nativeProps_DEPRECATED` changes, so a cloned node + * can tell whether its props already include them. + */ + mutable uint32_t nativePropsVersion_DEPRECATED{0}; + /** * @return tag for the ShadowNodeFamily. */ diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 7eefe4606177..b12c41795780 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -150,6 +150,7 @@ std::shared_ptr UIManager::cloneNode( *family.nativeProps_DEPRECATED, // source (folly::dynamic)rawProps, // patch NullValueStrategy::Ignore)); + family.nativePropsVersion_DEPRECATED++; // 2. Compute the final set of props. // @@ -465,6 +466,7 @@ void UIManager::setNativeProps_DEPRECATED( auto& family = shadowNode->getFamily(); { std::lock_guard lock(family.nativePropsMutex); + family.nativePropsVersion_DEPRECATED++; if (family.nativeProps_DEPRECATED) { // Values in `rawProps` patch (take precedence over) // `nativeProps_DEPRECATED`. For example, if both diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FabricUIManagerTest.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FabricUIManagerTest.cpp index 47aaa20ef567..f8c96566adda 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FabricUIManagerTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/FabricUIManagerTest.cpp @@ -155,4 +155,45 @@ TEST_F(FabricUIManagerTest, SetNativePropsMergesWithReactRender) { EXPECT_FLOAT_EQ(merged.opacity, 0.5); } +// Cloning a node whose props already include the props set by +// `setNativeProps_DEPRECATED` (as Yoga does for every child of a node it lays +// out) keeps those props instead of parsing them again. Props set later are +// still applied. +TEST_F(FabricUIManagerTest, CloneAppliesSetNativePropsOnlyOnce) { + uiManager_->setNativeProps_DEPRECATED( + currentViewNode(), RawProps(folly::dynamic::object("opacity", 0.5))); + + auto first = currentViewNode()->clone({}); + EXPECT_FLOAT_EQ( + static_cast(*first->getProps()).opacity, 0.5); + + auto second = first->clone({}); + EXPECT_EQ(second->getProps(), first->getProps()); + + uiManager_->setNativeProps_DEPRECATED( + currentViewNode(), RawProps(folly::dynamic::object("opacity", 0.25))); + auto third = second->clone({}); + EXPECT_NE(third->getProps(), second->getProps()); + EXPECT_FLOAT_EQ( + static_cast(*third->getProps()).opacity, 0.25); +} + +// After a React re-render replaces a node's props, the next clone applies the +// props set by `setNativeProps_DEPRECATED` again. +TEST_F(FabricUIManagerTest, CloneAppliesSetNativePropsAfterReactRender) { + uiManager_->setNativeProps_DEPRECATED( + currentViewNode(), RawProps(folly::dynamic::object("opacity", 0.5))); + auto applied = currentViewNode()->clone({}); + + auto rerendered = uiManager_->cloneNode( + *applied, + /*children=*/nullptr, + RawProps(folly::dynamic::object("nativeID", "from-react"))); + auto cloned = rerendered->clone({}); + EXPECT_NE(cloned->getProps(), rerendered->getProps()); + auto& props = static_cast(*cloned->getProps()); + EXPECT_EQ(props.nativeId, "from-react"); + EXPECT_FLOAT_EQ(props.opacity, 0.5); +} + } // namespace facebook::react