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;