From c7611e0d799f1e8312ed3b702e0433263eb3ed17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 15 May 2025 08:46:32 -0700 Subject: [PATCH] Add definition of setNativeProps in NativeDOM and use it if available (#51343) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/51343 Changelog: [internal] This implements `setNativeProps` as a method in the `NativeDOM` C++ TurboModule, so we can replace usages of the method from `FabricUIManager`. Reviewed By: javache Differential Revision: D74800815 fbshipit-source-id: cf3c2d4b75c8d30e2528f96d4d63318e1366096a --- ...tUpReactFabricPublicInstanceFantomTests.js | 54 +++++++++++++++++++ .../react/nativemodule/dom/NativeDOM.cpp | 20 +++++-- .../react/nativemodule/dom/NativeDOM.h | 7 +++ .../webapis/dom/nodes/ReactNativeElement.js | 10 +--- .../webapis/dom/nodes/specs/NativeDOM.js | 38 +++++++++++++ 5 files changed, 117 insertions(+), 12 deletions(-) diff --git a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js index 5fe719463ed..aaed636539a 100644 --- a/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js +++ b/packages/react-native/Libraries/ReactNative/ReactFabricPublicInstance/__tests__/setUpReactFabricPublicInstanceFantomTests.js @@ -14,6 +14,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; import type {HostInstance} from 'react-native'; import ReactNativeElement from '../../../../src/private/webapis/dom/nodes/ReactNativeElement'; +import {getRawNativeDOMForTests} from '../../../../src/private/webapis/dom/nodes/specs/NativeDOM'; import TextInputState from '../../../Components/TextInput/TextInputState'; import View from '../../../Components/View/View'; import ReactFabricHostComponent from '../ReactFabricHostComponent'; @@ -395,6 +396,59 @@ export default function setUpTests({isModern}: {isModern: boolean}) { .toJSX(), ).toEqual(); }); + + // TODO: delete when NativeDOM.setNativeProps is NOT nullable. + // This logic is to ensure compatibility with old app versions without the native module method. + if (isModern) { + let RawNativeDOM; + let originalSetNativeProps; + + beforeAll(() => { + RawNativeDOM = nullthrows(getRawNativeDOMForTests()); + originalSetNativeProps = RawNativeDOM.setNativeProps; + }); + + beforeEach(() => { + // $FlowExpectedError[cannot-write] + RawNativeDOM.setNativeProps = originalSetNativeProps; + }); + + it('should propagate changes to the host component (when NativeDOM.setNativeProps is not available)', () => { + // $FlowExpectedError[cannot-write] + RawNativeDOM.setNativeProps = null; + + expect(RawNativeDOM.setNativeProps).toBeNull(); + + const root = Fantom.createRoot(); + const nodeRef = React.createRef(); + + Fantom.runTask(() => { + root.render(); + }); + + expect( + root + .getRenderedOutput({ + props: ['testID'], + }) + .toJSX(), + ).toEqual(); + + const element = nullthrows(nodeRef.current); + + Fantom.runTask(() => { + element.setNativeProps({testID: 'second test id'}); + }); + + expect( + root + .getRenderedOutput({ + props: ['testID'], + }) + .toJSX(), + ).toEqual(); + }); + } }); }); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp index ae976409e75..f45115b2e90 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -25,12 +25,15 @@ namespace facebook::react { #pragma mark - Private helpers +static UIManager& getUIManagerFromRuntime(facebook::jsi::Runtime& runtime) { + return UIManagerBinding::getBinding(runtime)->getUIManager(); +} + static RootShadowNode::Shared getCurrentShadowTreeRevision( facebook::jsi::Runtime& runtime, SurfaceId surfaceId) { - auto& uiManager = - facebook::react::UIManagerBinding::getBinding(runtime)->getUIManager(); - auto shadowTreeRevisionProvider = uiManager.getShadowTreeRevisionProvider(); + auto shadowTreeRevisionProvider = + getUIManagerFromRuntime(runtime).getShadowTreeRevisionProvider(); return shadowTreeRevisionProvider->getCurrentRevision(surfaceId); } @@ -459,4 +462,15 @@ void NativeDOM::measureLayout( jsi::Value{rt, rect.height}}); } +#pragma mark - Legacy direct manipulation APIs (for `ReactNativeElement`). + +void NativeDOM::setNativeProps( + jsi::Runtime& rt, + jsi::Value nativeElementReference, + jsi::Value updatePayload) { + getUIManagerFromRuntime(rt).setNativeProps_DEPRECATED( + shadowNodeFromValue(rt, nativeElementReference), + RawProps(rt, updatePayload)); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h index 268cd1ba85f..889a4ae527a 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.h @@ -122,6 +122,13 @@ class NativeDOM : public NativeDOMCxxSpec { jsi::Value relativeToNativeElementReference, jsi::Function onFail, jsi::Function onSuccess); + +#pragma mark - Legacy direct manipulation APIs (for `ReactNativeElement`). + + void setNativeProps( + jsi::Runtime& rt, + jsi::Value nativeElementReference, + jsi::Value updatePayload); }; } // namespace facebook::react diff --git a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js index 7cca0cc8009..2d28f9d7787 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js +++ b/packages/react-native/src/private/webapis/dom/nodes/ReactNativeElement.js @@ -12,7 +12,6 @@ import type { InternalInstanceHandle, - Node as ShadowNode, ViewConfig, } from '../../../../../Libraries/Renderer/shims/ReactNativeTypes'; import type { @@ -26,7 +25,6 @@ import type {InstanceHandle} from './internals/NodeInternals'; import type ReactNativeDocument from './ReactNativeDocument'; import TextInputState from '../../../../../Libraries/Components/TextInput/TextInputState'; -import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIManager'; import {create as createAttributePayload} from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; import warnForStyleProps from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps'; import { @@ -37,7 +35,6 @@ import { } from './internals/NodeInternals'; import ReadOnlyElement, {getBoundingClientRect} from './ReadOnlyElement'; import NativeDOM from './specs/NativeDOM'; -import nullthrows from 'nullthrows'; const noop = () => {}; @@ -205,12 +202,7 @@ class ReactNativeElement extends ReadOnlyElement implements NativeMethods { const node = getNativeElementReference(this); if (node != null && updatePayload != null) { - // $FlowExpectedError[incompatible-type] This is an element instance so the native node reference is always a shadow node. - const shadowNode: ShadowNode = node; - nullthrows(getFabricUIManager()).setNativeProps( - shadowNode, - updatePayload, - ); + NativeDOM.setNativeProps(node, updatePayload); } } } diff --git a/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js b/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js index 79b073f47ac..60e23b2efe9 100644 --- a/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js +++ b/packages/react-native/src/private/webapis/dom/nodes/specs/NativeDOM.js @@ -13,6 +13,7 @@ import type {Node as ShadowNode} from '../../../../../../Libraries/Renderer/shim import type {TurboModule} from '../../../../../../Libraries/TurboModule/RCTExport'; import type {InstanceHandle} from '../internals/NodeInternals'; +import {getFabricUIManager} from '../../../../../../Libraries/ReactNative/FabricUIManager'; import * as TurboModuleRegistry from '../../../../../../Libraries/TurboModule/TurboModuleRegistry'; import nullthrows from 'nullthrows'; @@ -154,10 +155,23 @@ export interface Spec extends TurboModule { onFail: () => void, onSuccess: MeasureLayoutOnSuccessCallback, ) => void; + + /** + * Legacy direct manipulation APIs (for `ReactNativeElement`). + */ + + +setNativeProps?: ( + nativeElementReference: mixed, + updatePayload: mixed, + ) => void; } const RawNativeDOM = (TurboModuleRegistry.get('NativeDOMCxx'): ?Spec); +export function getRawNativeDOMForTests(): ?Spec { + return RawNativeDOM; +} + // This is the actual interface of this module, but the native module codegen // isn't expressive enough yet. export interface RefinedSpec { @@ -408,6 +422,14 @@ export interface RefinedSpec { onFail: () => void, onSuccess: MeasureLayoutOnSuccessCallback, ) => void; + + /** + * Legacy direct manipulation APIs + */ + +setNativeProps: ( + nativeElementReference: NativeElementReference, + updatePayload: {...}, + ) => void; } const NativeDOM: RefinedSpec = { @@ -574,6 +596,22 @@ const NativeDOM: RefinedSpec = { onSuccess, ); }, + + /** + * Legacy direct manipulation APIs + */ + setNativeProps(nativeNodeReference, updatePayload) { + // TODO: remove when RawNativeDOM.setNativeProps is NOT nullable. + if (RawNativeDOM?.setNativeProps == null) { + nullthrows(getFabricUIManager()).setNativeProps( + nativeNodeReference, + updatePayload, + ); + return; + } + + return RawNativeDOM.setNativeProps(nativeNodeReference, updatePayload); + }, }; export default NativeDOM;