diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js index f4f499a0b7d34..632bf3b366f49 100644 --- a/packages/react-native-renderer/src/ReactFabric.js +++ b/packages/react-native-renderer/src/ReactFabric.js @@ -39,6 +39,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; import warningWithoutStack from 'shared/warningWithoutStack'; +const {dispatchCommand: fabricDispatchCommand} = nativeFabricUIManager; + const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner; function findNodeHandle(componentOrHandle: any): ?number { @@ -116,6 +118,26 @@ const ReactFabric: ReactFabricType = { return; }, + dispatchCommand(handle: any, command: string, args: Array) { + const invalid = + handle._nativeTag == null || handle._internalInstanceHandle == null; + + if (invalid) { + warningWithoutStack( + !invalid, + "dispatchCommand was called with a ref that isn't a " + + 'native component. Use React.forwardRef to get access to the underlying native component', + ); + return; + } + + fabricDispatchCommand( + handle._internalInstanceHandle.stateNode.node, + command, + args, + ); + }, + 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 7c333f8148fbc..46685f4d098f1 100644 --- a/packages/react-native-renderer/src/ReactNativeRenderer.js +++ b/packages/react-native-renderer/src/ReactNativeRenderer.js @@ -120,6 +120,19 @@ const ReactNativeRenderer: ReactNativeType = { findNodeHandle, + dispatchCommand(handle: any, command: string, args: Array) { + if (handle._nativeTag == null) { + warningWithoutStack( + handle._nativeTag != null, + "dispatchCommand was called with a ref that isn't a " + + 'native component. Use React.forwardRef to get access to the underlying native component', + ); + return; + } + + UIManager.dispatchViewManagerCommand(handle._nativeTag, command, args); + }, + setNativeProps, render(element: React$Element, containerTag: any, callback: ?Function) { diff --git a/packages/react-native-renderer/src/ReactNativeTypes.js b/packages/react-native-renderer/src/ReactNativeTypes.js index 24b93a56c4d17..34e1ecc6eae1e 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, + dispatchCommand(handle: any, command: string, args: Array): void, setNativeProps(handle: any, nativeProps: Object): void, render( element: React$Element, @@ -147,6 +148,7 @@ export type ReactNativeType = { export type ReactFabricType = { NativeComponent: typeof ReactNativeComponent, findNodeHandle(componentOrHandle: any): ?number, + dispatchCommand(handle: any, command: string, args: Array): void, setNativeProps(handle: any, nativeProps: Object): void, render( element: React$Element, diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js index 84a00eeaa897b..268175bab8273 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js @@ -120,6 +120,8 @@ const RCTFabricUIManager = { roots.set(rootTag, newChildSet); }), + dispatchCommand: jest.fn(), + registerEventHandler: jest.fn(function registerEventHandler(callback) {}), measure: jest.fn(function measure(node, callback) { diff --git a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js index 43cd100f48a96..c77ce38031d8e 100644 --- a/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js +++ b/packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js @@ -88,6 +88,7 @@ const RCTUIManager = { viewName: viewName, }); }), + dispatchViewManagerCommand: jest.fn(), setJSResponder: jest.fn(), setChildren: jest.fn(function setChildren(parentTag, reactTags) { autoCreateRoot(parentTag); 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 524b4ed150b4c..e13718ad7374a 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js @@ -22,6 +22,10 @@ let NativeMethodsMixin; const SET_NATIVE_PROPS_NOT_SUPPORTED_MESSAGE = 'Warning: setNativeProps is not currently supported in Fabric'; +const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = + "Warning: dispatchCommand was called with a ref that isn't a " + + 'native component. Use React.forwardRef to get access to the underlying native component'; + jest.mock('shared/ReactFeatureFlags', () => require('shared/forks/ReactFeatureFlags.native-oss'), ); @@ -255,6 +259,85 @@ describe('ReactFabric', () => { }); }); + it('should call dispatchCommand for native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + [View].forEach(Component => { + nativeFabricUIManager.dispatchCommand.mockClear(); + + let viewRef; + ReactFabric.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled(); + ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + expect(nativeFabricUIManager.dispatchCommand).toHaveBeenCalledTimes(1); + expect(nativeFabricUIManager.dispatchCommand).toHaveBeenCalledWith( + expect.any(Object), + 'updateCommand', + [10, 20], + ); + }); + }); + + it('should warn and no-op if calling dispatchCommand 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 => { + nativeFabricUIManager.dispatchCommand.mockReset(); + + let viewRef; + ReactFabric.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled(); + expect(() => { + ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + }).toWarnDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + withoutStack: true, + }); + + expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled(); + }); + }); + it('setNativeProps on native refs should no-op', () => { 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 74e2ab2a47e32..10f3bda5522ef 100644 --- a/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js @@ -54,6 +54,26 @@ describe('ReactFabric', () => { expect(handle).toBe(2); }); + it('dispatches commands on Fabric nodes with the RN renderer', () => { + UIManager.dispatchViewManagerCommand.mockReset(); + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {title: true}, + uiViewClassName: 'RCTView', + })); + + let ref = React.createRef(); + + ReactFabric.render(, 11); + expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); + ReactNative.dispatchCommand(ref.current, 'myCommand', [10, 20]); + expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledTimes(1); + expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledWith( + expect.any(Number), + 'myCommand', + [10, 20], + ); + }); + it('sets native props with setNativeProps on Fabric nodes with the RN renderer', () => { UIManager.updateView.mockReset(); const View = createReactNativeComponentClass('RCTView', () => ({ 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 8d3f0bb28954b..ce51af605f4b5 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js @@ -19,6 +19,10 @@ let createReactNativeComponentClass; let UIManager; let NativeMethodsMixin; +const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT = + "Warning: dispatchCommand was called with a ref that isn't a " + + 'native component. Use React.forwardRef to get access to the underlying native component'; + const SET_NATIVE_PROPS_DEPRECATION_MESSAGE = 'Warning: Calling ref.setNativeProps(nativeProps) ' + 'is deprecated and will be removed in a future release. ' + @@ -108,6 +112,85 @@ describe('ReactNative', () => { expect(UIManager.updateView).toHaveBeenCalledTimes(4); }); + it('should call dispatchCommand for native refs', () => { + const View = createReactNativeComponentClass('RCTView', () => ({ + validAttributes: {foo: true}, + uiViewClassName: 'RCTView', + })); + + [View].forEach(Component => { + UIManager.dispatchViewManagerCommand.mockClear(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); + ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledTimes(1); + expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledWith( + expect.any(Number), + 'updateCommand', + [10, 20], + ); + }); + }); + + it('should warn and no-op if calling dispatchCommand 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.dispatchViewManagerCommand.mockReset(); + + let viewRef; + ReactNative.render( + { + viewRef = ref; + }} + />, + 11, + ); + + expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); + expect(() => { + ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]); + }).toWarnDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], { + withoutStack: true, + }); + + expect(UIManager.dispatchViewManagerCommand).not.toBeCalled(); + }); + }); + it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => { const View = createReactNativeComponentClass('RCTView', () => ({ validAttributes: {foo: true}, diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index f90d1e4d94742..946f6997415d9 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -44,6 +44,11 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' rootTag: number, props: ?Object, ) => void, + dispatchViewManagerCommand: ( + reactTag: number, + command: string, + args: Array, + ) => void, manageChildren: ( containerTag: number, moveFromIndices: Array, @@ -120,6 +125,8 @@ declare var nativeFabricUIManager: { ) => void, ) => void, + dispatchCommand: (node: Object, command: string, args: Array) => void, + measure: (node: Node, callback: MeasureOnSuccessCallback) => void, measureInWindow: ( node: Node,