[Fabric] Add dispatchCommand to React Native renderers (#16085)
* Add dispatchCommand to the public export of the React Native renderers * Fixup invalid check * Prettier * Prettier
Eli White committed
Jul 8, 2019 at 13:03 UTC
8533c0a168138d2442334021751b42e530d08bb8
9 files changed
+233
packages/react-native-renderer/src/ReactFabric.js
+22
@@ -39,6 +39,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
39
import getComponentName from 'shared/getComponentName';
40
import warningWithoutStack from 'shared/warningWithoutStack';
41
42
+const {dispatchCommand: fabricDispatchCommand} = nativeFabricUIManager;
43
+
44
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
45
46
function findNodeHandle(componentOrHandle: any): ?number {
@@ -116,6 +118,26 @@ const ReactFabric: ReactFabricType = {
118
return;
119
},
120
121
+ dispatchCommand(handle: any, command: string, args: Array<any>) {
122
+ const invalid =
123
+ handle._nativeTag == null || handle._internalInstanceHandle == null;
124
+
125
+ if (invalid) {
126
+ warningWithoutStack(
127
+ !invalid,
128
+ "dispatchCommand was called with a ref that isn't a " +
129
+ 'native component. Use React.forwardRef to get access to the underlying native component',
130
+ );
131
+ return;
132
+ }
133
+
134
+ fabricDispatchCommand(
135
+ handle._internalInstanceHandle.stateNode.node,
136
+ command,
137
+ args,
138
+ );
139
+ },
140
+
141
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
142
let root = roots.get(containerTag);
143
packages/react-native-renderer/src/ReactNativeRenderer.js
+13
@@ -120,6 +120,19 @@ const ReactNativeRenderer: ReactNativeType = {
120
121
findNodeHandle,
122
123
+ dispatchCommand(handle: any, command: string, args: Array<any>) {
124
+ if (handle._nativeTag == null) {
125
+ warningWithoutStack(
126
+ handle._nativeTag != null,
127
+ "dispatchCommand was called with a ref that isn't a " +
128
+ 'native component. Use React.forwardRef to get access to the underlying native component',
129
+ );
130
+ return;
131
+ }
132
+
133
+ UIManager.dispatchViewManagerCommand(handle._nativeTag, command, args);
134
+ },
135
+
136
setNativeProps,
137
138
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
packages/react-native-renderer/src/ReactNativeTypes.js
+2
@@ -131,6 +131,7 @@ type SecretInternalsFabricType = {
131
export type ReactNativeType = {
132
NativeComponent: typeof ReactNativeComponent,
133
findNodeHandle(componentOrHandle: any): ?number,
134
+ dispatchCommand(handle: any, command: string, args: Array<any>): void,
135
setNativeProps(handle: any, nativeProps: Object): void,
136
render(
137
element: React$Element<any>,
@@ -147,6 +148,7 @@ export type ReactNativeType = {
148
export type ReactFabricType = {
149
NativeComponent: typeof ReactNativeComponent,
150
findNodeHandle(componentOrHandle: any): ?number,
151
+ dispatchCommand(handle: any, command: string, args: Array<any>): void,
152
setNativeProps(handle: any, nativeProps: Object): void,
153
render(
154
element: React$Element<any>,
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js
+2
@@ -120,6 +120,8 @@ const RCTFabricUIManager = {
120
roots.set(rootTag, newChildSet);
121
}),
122
123
+ dispatchCommand: jest.fn(),
124
+
125
registerEventHandler: jest.fn(function registerEventHandler(callback) {}),
126
127
measure: jest.fn(function measure(node, callback) {
packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js
+1
@@ -88,6 +88,7 @@ const RCTUIManager = {
88
viewName: viewName,
89
});
90
}),
91
+ dispatchViewManagerCommand: jest.fn(),
92
setJSResponder: jest.fn(),
93
setChildren: jest.fn(function setChildren(parentTag, reactTags) {
94
autoCreateRoot(parentTag);
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+83
@@ -22,6 +22,10 @@ let NativeMethodsMixin;
22
const SET_NATIVE_PROPS_NOT_SUPPORTED_MESSAGE =
23
'Warning: setNativeProps is not currently supported in Fabric';
24
25
+const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
26
+ "Warning: dispatchCommand was called with a ref that isn't a " +
27
+ 'native component. Use React.forwardRef to get access to the underlying native component';
28
+
29
jest.mock('shared/ReactFeatureFlags', () =>
30
require('shared/forks/ReactFeatureFlags.native-oss'),
31
);
@@ -255,6 +259,85 @@ describe('ReactFabric', () => {
259
});
260
});
261
262
+ it('should call dispatchCommand for native refs', () => {
263
+ const View = createReactNativeComponentClass('RCTView', () => ({
264
+ validAttributes: {foo: true},
265
+ uiViewClassName: 'RCTView',
266
+ }));
267
+
268
+ [View].forEach(Component => {
269
+ nativeFabricUIManager.dispatchCommand.mockClear();
270
+
271
+ let viewRef;
272
+ ReactFabric.render(
273
+ <Component
274
+ ref={ref => {
275
+ viewRef = ref;
276
+ }}
277
+ />,
278
+ 11,
279
+ );
280
+
281
+ expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
282
+ ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]);
283
+ expect(nativeFabricUIManager.dispatchCommand).toHaveBeenCalledTimes(1);
284
+ expect(nativeFabricUIManager.dispatchCommand).toHaveBeenCalledWith(
285
+ expect.any(Object),
286
+ 'updateCommand',
287
+ [10, 20],
288
+ );
289
+ });
290
+ });
291
+
292
+ it('should warn and no-op if calling dispatchCommand on non native refs', () => {
293
+ const View = createReactNativeComponentClass('RCTView', () => ({
294
+ validAttributes: {foo: true},
295
+ uiViewClassName: 'RCTView',
296
+ }));
297
+
298
+ class BasicClass extends React.Component {
299
+ render() {
300
+ return <React.Fragment />;
301
+ }
302
+ }
303
+
304
+ class Subclass extends ReactFabric.NativeComponent {
305
+ render() {
306
+ return <View />;
307
+ }
308
+ }
309
+
310
+ const CreateClass = createReactClass({
311
+ mixins: [NativeMethodsMixin],
312
+ render: () => {
313
+ return <View />;
314
+ },
315
+ });
316
+
317
+ [BasicClass, Subclass, CreateClass].forEach(Component => {
318
+ nativeFabricUIManager.dispatchCommand.mockReset();
319
+
320
+ let viewRef;
321
+ ReactFabric.render(
322
+ <Component
323
+ ref={ref => {
324
+ viewRef = ref;
325
+ }}
326
+ />,
327
+ 11,
328
+ );
329
+
330
+ expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
331
+ expect(() => {
332
+ ReactFabric.dispatchCommand(viewRef, 'updateCommand', [10, 20]);
333
+ }).toWarnDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], {
334
+ withoutStack: true,
335
+ });
336
+
337
+ expect(nativeFabricUIManager.dispatchCommand).not.toBeCalled();
338
+ });
339
+ });
340
+
341
it('setNativeProps on native refs should no-op', () => {
342
const View = createReactNativeComponentClass('RCTView', () => ({
343
validAttributes: {foo: true},
packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js
+20
@@ -54,6 +54,26 @@ describe('ReactFabric', () => {
54
expect(handle).toBe(2);
55
});
56
57
+ it('dispatches commands on Fabric nodes with the RN renderer', () => {
58
+ UIManager.dispatchViewManagerCommand.mockReset();
59
+ const View = createReactNativeComponentClass('RCTView', () => ({
60
+ validAttributes: {title: true},
61
+ uiViewClassName: 'RCTView',
62
+ }));
63
+
64
+ let ref = React.createRef();
65
+
66
+ ReactFabric.render(<View title="bar" ref={ref} />, 11);
67
+ expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
68
+ ReactNative.dispatchCommand(ref.current, 'myCommand', [10, 20]);
69
+ expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledTimes(1);
70
+ expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledWith(
71
+ expect.any(Number),
72
+ 'myCommand',
73
+ [10, 20],
74
+ );
75
+ });
76
+
77
it('sets native props with setNativeProps on Fabric nodes with the RN renderer', () => {
78
UIManager.updateView.mockReset();
79
const View = createReactNativeComponentClass('RCTView', () => ({
packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js
+83
@@ -19,6 +19,10 @@ let createReactNativeComponentClass;
19
let UIManager;
20
let NativeMethodsMixin;
21
22
+const DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT =
23
+ "Warning: dispatchCommand was called with a ref that isn't a " +
24
+ 'native component. Use React.forwardRef to get access to the underlying native component';
25
+
26
const SET_NATIVE_PROPS_DEPRECATION_MESSAGE =
27
'Warning: Calling ref.setNativeProps(nativeProps) ' +
28
'is deprecated and will be removed in a future release. ' +
@@ -108,6 +112,85 @@ describe('ReactNative', () => {
112
expect(UIManager.updateView).toHaveBeenCalledTimes(4);
113
});
114
115
+ it('should call dispatchCommand for native refs', () => {
116
+ const View = createReactNativeComponentClass('RCTView', () => ({
117
+ validAttributes: {foo: true},
118
+ uiViewClassName: 'RCTView',
119
+ }));
120
+
121
+ [View].forEach(Component => {
122
+ UIManager.dispatchViewManagerCommand.mockClear();
123
+
124
+ let viewRef;
125
+ ReactNative.render(
126
+ <Component
127
+ ref={ref => {
128
+ viewRef = ref;
129
+ }}
130
+ />,
131
+ 11,
132
+ );
133
+
134
+ expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
135
+ ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]);
136
+ expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledTimes(1);
137
+ expect(UIManager.dispatchViewManagerCommand).toHaveBeenCalledWith(
138
+ expect.any(Number),
139
+ 'updateCommand',
140
+ [10, 20],
141
+ );
142
+ });
143
+ });
144
+
145
+ it('should warn and no-op if calling dispatchCommand on non native refs', () => {
146
+ const View = createReactNativeComponentClass('RCTView', () => ({
147
+ validAttributes: {foo: true},
148
+ uiViewClassName: 'RCTView',
149
+ }));
150
+
151
+ class BasicClass extends React.Component {
152
+ render() {
153
+ return <React.Fragment />;
154
+ }
155
+ }
156
+
157
+ class Subclass extends ReactNative.NativeComponent {
158
+ render() {
159
+ return <View />;
160
+ }
161
+ }
162
+
163
+ const CreateClass = createReactClass({
164
+ mixins: [NativeMethodsMixin],
165
+ render: () => {
166
+ return <View />;
167
+ },
168
+ });
169
+
170
+ [BasicClass, Subclass, CreateClass].forEach(Component => {
171
+ UIManager.dispatchViewManagerCommand.mockReset();
172
+
173
+ let viewRef;
174
+ ReactNative.render(
175
+ <Component
176
+ ref={ref => {
177
+ viewRef = ref;
178
+ }}
179
+ />,
180
+ 11,
181
+ );
182
+
183
+ expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
184
+ expect(() => {
185
+ ReactNative.dispatchCommand(viewRef, 'updateCommand', [10, 20]);
186
+ }).toWarnDev([DISPATCH_COMMAND_REQUIRES_HOST_COMPONENT], {
187
+ withoutStack: true,
188
+ });
189
+
190
+ expect(UIManager.dispatchViewManagerCommand).not.toBeCalled();
191
+ });
192
+ });
193
+
194
it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => {
195
const View = createReactNativeComponentClass('RCTView', () => ({
196
validAttributes: {foo: true},
scripts/flow/react-native-host-hooks.js
+7
@@ -44,6 +44,11 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
44
rootTag: number,
45
props: ?Object,
46
) => void,
47
+ dispatchViewManagerCommand: (
48
+ reactTag: number,
49
+ command: string,
50
+ args: Array<any>,
51
+ ) => void,
52
manageChildren: (
53
containerTag: number,
54
moveFromIndices: Array<number>,
@@ -120,6 +125,8 @@ declare var nativeFabricUIManager: {
125
) => void,
126
) => void,
127
128
+ dispatchCommand: (node: Object, command: string, args: Array<any>) => void,
129
+
130
measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
131
measureInWindow: (
132
node: Node,