Skip to content

Commit 8533c0a

Browse files
authored
[Fabric] Add dispatchCommand to React Native renderers (#16085)
* Add dispatchCommand to the public export of the React Native renderers * Fixup invalid check * Prettier * Prettier
1 parent 2253bc8 commit 8533c0a

File tree

9 files changed

+233
-0
lines changed

9 files changed

+233
-0
lines changed

packages/react-native-renderer/src/ReactFabric.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import ReactSharedInternals from 'shared/ReactSharedInternals';
3939
import getComponentName from 'shared/getComponentName';
4040
import warningWithoutStack from 'shared/warningWithoutStack';
4141

42+
const {dispatchCommand: fabricDispatchCommand} = nativeFabricUIManager;
43+
4244
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
4345

4446
function findNodeHandle(componentOrHandle: any): ?number {
@@ -116,6 +118,26 @@ const ReactFabric: ReactFabricType = {
116118
return;
117119
},
118120

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+
119141
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
120142
let root = roots.get(containerTag);
121143

packages/react-native-renderer/src/ReactNativeRenderer.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ const ReactNativeRenderer: ReactNativeType = {
120120

121121
findNodeHandle,
122122

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+
123136
setNativeProps,
124137

125138
render(element: React$Element<any>, containerTag: any, callback: ?Function) {

packages/react-native-renderer/src/ReactNativeTypes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ type SecretInternalsFabricType = {
131131
export type ReactNativeType = {
132132
NativeComponent: typeof ReactNativeComponent,
133133
findNodeHandle(componentOrHandle: any): ?number,
134+
dispatchCommand(handle: any, command: string, args: Array<any>): void,
134135
setNativeProps(handle: any, nativeProps: Object): void,
135136
render(
136137
element: React$Element<any>,
@@ -147,6 +148,7 @@ export type ReactNativeType = {
147148
export type ReactFabricType = {
148149
NativeComponent: typeof ReactNativeComponent,
149150
findNodeHandle(componentOrHandle: any): ?number,
151+
dispatchCommand(handle: any, command: string, args: Array<any>): void,
150152
setNativeProps(handle: any, nativeProps: Object): void,
151153
render(
152154
element: React$Element<any>,

packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/InitializeNativeFabricUIManager.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ const RCTFabricUIManager = {
120120
roots.set(rootTag, newChildSet);
121121
}),
122122

123+
dispatchCommand: jest.fn(),
124+
123125
registerEventHandler: jest.fn(function registerEventHandler(callback) {}),
124126

125127
measure: jest.fn(function measure(node, callback) {

packages/react-native-renderer/src/__mocks__/react-native/Libraries/ReactPrivate/UIManager.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const RCTUIManager = {
8888
viewName: viewName,
8989
});
9090
}),
91+
dispatchViewManagerCommand: jest.fn(),
9192
setJSResponder: jest.fn(),
9293
setChildren: jest.fn(function setChildren(parentTag, reactTags) {
9394
autoCreateRoot(parentTag);

packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ let NativeMethodsMixin;
2222
const SET_NATIVE_PROPS_NOT_SUPPORTED_MESSAGE =
2323
'Warning: setNativeProps is not currently supported in Fabric';
2424

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+
2529
jest.mock('shared/ReactFeatureFlags', () =>
2630
require('shared/forks/ReactFeatureFlags.native-oss'),
2731
);
@@ -255,6 +259,85 @@ describe('ReactFabric', () => {
255259
});
256260
});
257261

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+
258341
it('setNativeProps on native refs should no-op', () => {
259342
const View = createReactNativeComponentClass('RCTView', () => ({
260343
validAttributes: {foo: true},

packages/react-native-renderer/src/__tests__/ReactFabricAndNative-test.internal.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,26 @@ describe('ReactFabric', () => {
5454
expect(handle).toBe(2);
5555
});
5656

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+
5777
it('sets native props with setNativeProps on Fabric nodes with the RN renderer', () => {
5878
UIManager.updateView.mockReset();
5979
const View = createReactNativeComponentClass('RCTView', () => ({

packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ let createReactNativeComponentClass;
1919
let UIManager;
2020
let NativeMethodsMixin;
2121

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+
2226
const SET_NATIVE_PROPS_DEPRECATION_MESSAGE =
2327
'Warning: Calling ref.setNativeProps(nativeProps) ' +
2428
'is deprecated and will be removed in a future release. ' +
@@ -108,6 +112,85 @@ describe('ReactNative', () => {
108112
expect(UIManager.updateView).toHaveBeenCalledTimes(4);
109113
});
110114

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+
111194
it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => {
112195
const View = createReactNativeComponentClass('RCTView', () => ({
113196
validAttributes: {foo: true},

scripts/flow/react-native-host-hooks.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'
4444
rootTag: number,
4545
props: ?Object,
4646
) => void,
47+
dispatchViewManagerCommand: (
48+
reactTag: number,
49+
command: string,
50+
args: Array<any>,
51+
) => void,
4752
manageChildren: (
4853
containerTag: number,
4954
moveFromIndices: Array<number>,
@@ -120,6 +125,8 @@ declare var nativeFabricUIManager: {
120125
) => void,
121126
) => void,
122127

128+
dispatchCommand: (node: Object, command: string, args: Array<any>) => void,
129+
123130
measure: (node: Node, callback: MeasureOnSuccessCallback) => void,
124131
measureInWindow: (
125132
node: Node,

0 commit comments

Comments
 (0)