diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index 6f296545cba2a..b6fe0c65d7be0 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -32,6 +32,7 @@ import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeComponent from './ReactNativeComponent'; import {getClosestInstanceFromNode} from './ReactFabricComponentTree'; import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; +import {setNativeProps} from './ReactNativeRendererSharedExports'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; @@ -104,6 +105,8 @@ const ReactFabric: ReactFabricType = { findNodeHandle, + setNativeProps, + render(element: React$Element, containerTag: any, callback: ?Function) { let root = roots.get(containerTag); diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js index e26d6c5ee8ecf..6cfe575dcb3a9 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -38,6 +38,7 @@ import NativeMethodsMixin from './NativeMethodsMixin'; import ReactNativeComponent from './ReactNativeComponent'; import {getClosestInstanceFromNode} from './ReactNativeComponentTree'; import {getInspectorDataForViewTag} from './ReactNativeFiberInspector'; +import {setNativeProps} from './ReactNativeRendererSharedExports'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; @@ -116,6 +117,8 @@ const ReactNativeRenderer: ReactNativeType = { findNodeHandle, + setNativeProps, + render(element: React$Element, containerTag: any, callback: ?Function) { let root = roots.get(containerTag); diff --git a/packages/react-native-renderer/src/ReactNativeRendererSharedExports.js b/packages/react-native-renderer/src/ReactNativeRendererSharedExports.js new file mode 100644 index 0000000000000..ddfd296fd2082 --- /dev/null +++ b/packages/react-native-renderer/src/ReactNativeRendererSharedExports.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {create} from './ReactNativeAttributePayload'; +import {warnForStyleProps} from './NativeMethodsMixinUtils'; + +import warningWithoutStack from 'shared/warningWithoutStack'; + +// Module provided by RN: +import UIManager from 'UIManager'; + +export function setNativeProps(handle: any, nativeProps: Object): void { + if (handle._nativeTag == null) { + warningWithoutStack( + handle._nativeTag != null, + "setNativeProps was called on a ref that isn't a " + + 'native component. Use React.forwardRef to get access to the underlying native component', + ); + return; + } + + if (__DEV__) { + warnForStyleProps(nativeProps, handle.viewConfig.validAttributes); + } + + const updatePayload = create(nativeProps, handle.viewConfig.validAttributes); + // Avoid the overhead of bridge calls if there's no update. + // This is an expensive no-op for Android, and causes an unnecessary + // view invalidation for certain components (eg RCTTextInput) on iOS. + if (updatePayload != null) { + UIManager.updateView( + handle._nativeTag, + handle.viewConfig.uiViewClassName, + updatePayload, + ); + } +} diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index f9058abc1e939..1e145ed5e1d5c 100644 --- a/packages/react-native-renderer/src/ReactNativeTypes.js +++ b/packages/react-native-renderer/src/ReactNativeTypes.js @@ -131,6 +131,7 @@ type SecretInternalsFabricType = { export type ReactNativeType = { NativeComponent: typeof ReactNativeComponent, findNodeHandle(componentOrHandle: any): ?number, + setNativeProps(handle: any, nativeProps: Object): void, render( element: React$Element, containerTag: any, @@ -146,6 +147,7 @@ export type ReactNativeType = { export type ReactFabricType = { NativeComponent: typeof ReactNativeComponent, findNodeHandle(componentOrHandle: any): ?number, + setNativeProps(handle: any, nativeProps: Object): void, render( element: React$Element, containerTag: any, diff --git a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js index a045729e972d3..90ad6c3e80b4b 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -167,7 +167,7 @@ describe('ReactFabric', () => { expect(FabricUIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot(); }); - it('should not call UIManager.updateView from setNativeProps for properties that have not changed', () => { + it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, uiViewClassName: 'RCTView', @@ -214,6 +214,90 @@ describe('ReactFabric', () => { }); }); + it('should be able to setNativeProps on native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + UIManager.updateView.mockReset(); + + let viewRef; + ReactFabric.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.updateView).not.toBeCalled(); + ReactFabric.setNativeProps(viewRef, {foo: 'baz'}); + expect(UIManager.updateView).toHaveBeenCalledTimes(1); + expect(UIManager.updateView).toHaveBeenCalledWith( + expect.any(Number), + 'RCTView', + {foo: 'baz'}, + ); + }); + + it('should warn and no-op if calling setNativeProps on non native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class BasicClass extends React.Component { + render() { + return ; + } + } + + class Subclass extends ReactFabric.NativeComponent { + render() { + return ; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render: () => { + return ; + }, + }); + + [BasicClass, Subclass, CreateClass].forEach(Component => { + UIManager.updateView.mockReset(); + + let viewRef; + ReactFabric.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.updateView).not.toBeCalled(); + expect(() => { + ReactFabric.setNativeProps(viewRef, {foo: 'baz'}); + }).toWarnDev( + [ + "Warning: setNativeProps was called on a ref that isn't a " + + 'native component. Use React.forwardRef to get access ' + + 'to the underlying native component', + ], + {withoutStack: true}, + ); + + expect(UIManager.updateView).not.toBeCalled(); + }); + }); + it('returns the correct instance and calls it in the callback', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js index 6381235257e2a..1fff126bb1bba 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -13,12 +13,14 @@ let React; let ReactFabric; let ReactNative; +let UIManager; let createReactNativeComponentClass; describe('ReactFabric', () => { beforeEach(() => { jest.resetModules(); ReactNative = require('react-native-renderer'); + UIManager = require('UIManager'); jest.resetModules(); jest.mock('shared/ReactFeatureFlags', () => require('shared/forks/ReactFeatureFlags.native-oss'), @@ -49,4 +51,24 @@ describe('ReactFabric', () => { let handle = ReactNative.findNodeHandle(ref.current); expect(handle).toBe(2); }); + + it('sets native props with setNativeProps on Fabric nodes with the RN renderer', () => { + UIManager.updateView.mockReset(); + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {title: true}, + uiViewClassName: 'RCTView', + })); + + let ref = React.createRef(); + + ReactFabric.render(, 11); + expect(UIManager.updateView).not.toBeCalled(); + ReactNative.setNativeProps(ref.current, {title: 'baz'}); + expect(UIManager.updateView).toHaveBeenCalledTimes(1); + expect(UIManager.updateView).toHaveBeenCalledWith( + expect.any(Number), + 'RCTView', + {title: 'baz'}, + ); + }); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js index 8089088f8faea..225c941b3981f 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -145,6 +145,90 @@ describe('ReactNative', () => { }); }); + it('should be able to setNativeProps on native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + UIManager.updateView.mockReset(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.updateView).not.toBeCalled(); + ReactNative.setNativeProps(viewRef, {foo: 'baz'}); + expect(UIManager.updateView).toHaveBeenCalledTimes(1); + expect(UIManager.updateView).toHaveBeenCalledWith( + expect.any(Number), + 'RCTView', + {foo: 'baz'}, + ); + }); + + it('should warn and no-op if calling setNativeProps on non native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + class BasicClass extends React.Component { + render() { + return ; + } + } + + class Subclass extends ReactNative.NativeComponent { + render() { + return ; + } + } + + const CreateClass = createReactClass({ + mixins: [NativeMethodsMixin], + render: () => { + return ; + }, + }); + + [BasicClass, Subclass, CreateClass].forEach(Component => { + UIManager.updateView.mockReset(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.updateView).not.toBeCalled(); + expect(() => { + ReactNative.setNativeProps(viewRef, {foo: 'baz'}); + }).toWarnDev( + [ + "Warning: setNativeProps was called on a ref that isn't a " + + 'native component. Use React.forwardRef to get access ' + + 'to the underlying native component', + ], + {withoutStack: true}, + ); + + expect(UIManager.updateView).not.toBeCalled(); + }); + }); + it('returns the correct instance and calls it in the callback', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true},