Skip to content

Commit b0f45c0

Browse files
authored
Adding ReactNative.setNativeProps that takes a ref (#14907)
* Adding ReactNative.setNativeProps that takes a ref * Adding test for components rendered with Fabric with Paper's setNativeProps * Fixing flow types * Fix prettier * Rename ReactNativeSetNativeProps.js to be more general
1 parent 4f4aa69 commit b0f45c0

File tree

7 files changed

+242
-1
lines changed

7 files changed

+242
-1
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import NativeMethodsMixin from './NativeMethodsMixin';
3232
import ReactNativeComponent from './ReactNativeComponent';
3333
import {getClosestInstanceFromNode} from './ReactFabricComponentTree';
3434
import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
35+
import {setNativeProps} from './ReactNativeRendererSharedExports';
3536

3637
import ReactSharedInternals from 'shared/ReactSharedInternals';
3738
import getComponentName from 'shared/getComponentName';
@@ -104,6 +105,8 @@ const ReactFabric: ReactFabricType = {
104105

105106
findNodeHandle,
106107

108+
setNativeProps,
109+
107110
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
108111
let root = roots.get(containerTag);
109112

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import NativeMethodsMixin from './NativeMethodsMixin';
3838
import ReactNativeComponent from './ReactNativeComponent';
3939
import {getClosestInstanceFromNode} from './ReactNativeComponentTree';
4040
import {getInspectorDataForViewTag} from './ReactNativeFiberInspector';
41+
import {setNativeProps} from './ReactNativeRendererSharedExports';
4142

4243
import ReactSharedInternals from 'shared/ReactSharedInternals';
4344
import getComponentName from 'shared/getComponentName';
@@ -116,6 +117,8 @@ const ReactNativeRenderer: ReactNativeType = {
116117

117118
findNodeHandle,
118119

120+
setNativeProps,
121+
119122
render(element: React$Element<any>, containerTag: any, callback: ?Function) {
120123
let root = roots.get(containerTag);
121124

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {create} from './ReactNativeAttributePayload';
11+
import {warnForStyleProps} from './NativeMethodsMixinUtils';
12+
13+
import warningWithoutStack from 'shared/warningWithoutStack';
14+
15+
// Module provided by RN:
16+
import UIManager from 'UIManager';
17+
18+
export function setNativeProps(handle: any, nativeProps: Object): void {
19+
if (handle._nativeTag == null) {
20+
warningWithoutStack(
21+
handle._nativeTag != null,
22+
"setNativeProps was called on a ref that isn't a " +
23+
'native component. Use React.forwardRef to get access to the underlying native component',
24+
);
25+
return;
26+
}
27+
28+
if (__DEV__) {
29+
warnForStyleProps(nativeProps, handle.viewConfig.validAttributes);
30+
}
31+
32+
const updatePayload = create(nativeProps, handle.viewConfig.validAttributes);
33+
// Avoid the overhead of bridge calls if there's no update.
34+
// This is an expensive no-op for Android, and causes an unnecessary
35+
// view invalidation for certain components (eg RCTTextInput) on iOS.
36+
if (updatePayload != null) {
37+
UIManager.updateView(
38+
handle._nativeTag,
39+
handle.viewConfig.uiViewClassName,
40+
updatePayload,
41+
);
42+
}
43+
}

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+
setNativeProps(handle: any, nativeProps: Object): void,
134135
render(
135136
element: React$Element<any>,
136137
containerTag: any,
@@ -146,6 +147,7 @@ export type ReactNativeType = {
146147
export type ReactFabricType = {
147148
NativeComponent: typeof ReactNativeComponent,
148149
findNodeHandle(componentOrHandle: any): ?number,
150+
setNativeProps(handle: any, nativeProps: Object): void,
149151
render(
150152
element: React$Element<any>,
151153
containerTag: any,

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe('ReactFabric', () => {
167167
expect(FabricUIManager.__dumpHierarchyForJestTestsOnly()).toMatchSnapshot();
168168
});
169169

170-
it('should not call UIManager.updateView from setNativeProps for properties that have not changed', () => {
170+
it('should not call UIManager.updateView from ref.setNativeProps for properties that have not changed', () => {
171171
const View = createReactNativeComponentClass('RCTView', () => ({
172172
validAttributes: {foo: true},
173173
uiViewClassName: 'RCTView',
@@ -214,6 +214,90 @@ describe('ReactFabric', () => {
214214
});
215215
});
216216

217+
it('should be able to setNativeProps on native refs', () => {
218+
const View = createReactNativeComponentClass('RCTView', () => ({
219+
validAttributes: {foo: true},
220+
uiViewClassName: 'RCTView',
221+
}));
222+
223+
UIManager.updateView.mockReset();
224+
225+
let viewRef;
226+
ReactFabric.render(
227+
<View
228+
foo="bar"
229+
ref={ref => {
230+
viewRef = ref;
231+
}}
232+
/>,
233+
11,
234+
);
235+
236+
expect(UIManager.updateView).not.toBeCalled();
237+
ReactFabric.setNativeProps(viewRef, {foo: 'baz'});
238+
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
239+
expect(UIManager.updateView).toHaveBeenCalledWith(
240+
expect.any(Number),
241+
'RCTView',
242+
{foo: 'baz'},
243+
);
244+
});
245+
246+
it('should warn and no-op if calling setNativeProps on non native refs', () => {
247+
const View = createReactNativeComponentClass('RCTView', () => ({
248+
validAttributes: {foo: true},
249+
uiViewClassName: 'RCTView',
250+
}));
251+
252+
class BasicClass extends React.Component {
253+
render() {
254+
return <React.Fragment />;
255+
}
256+
}
257+
258+
class Subclass extends ReactFabric.NativeComponent {
259+
render() {
260+
return <View />;
261+
}
262+
}
263+
264+
const CreateClass = createReactClass({
265+
mixins: [NativeMethodsMixin],
266+
render: () => {
267+
return <View />;
268+
},
269+
});
270+
271+
[BasicClass, Subclass, CreateClass].forEach(Component => {
272+
UIManager.updateView.mockReset();
273+
274+
let viewRef;
275+
ReactFabric.render(
276+
<Component
277+
foo="bar"
278+
ref={ref => {
279+
viewRef = ref;
280+
}}
281+
/>,
282+
11,
283+
);
284+
285+
expect(UIManager.updateView).not.toBeCalled();
286+
expect(() => {
287+
ReactFabric.setNativeProps(viewRef, {foo: 'baz'});
288+
}).toWarnDev(
289+
[
290+
"Warning: setNativeProps was called on a ref that isn't a " +
291+
'native component. Use React.forwardRef to get access ' +
292+
'to the underlying native component',
293+
],
294+
{withoutStack: true},
295+
);
296+
297+
expect(UIManager.updateView).not.toBeCalled();
298+
});
299+
});
300+
217301
it('returns the correct instance and calls it in the callback', () => {
218302
const View = createReactNativeComponentClass('RCTView', () => ({
219303
validAttributes: {foo: true},

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
let React;
1414
let ReactFabric;
1515
let ReactNative;
16+
let UIManager;
1617
let createReactNativeComponentClass;
1718

1819
describe('ReactFabric', () => {
1920
beforeEach(() => {
2021
jest.resetModules();
2122
ReactNative = require('react-native-renderer');
23+
UIManager = require('UIManager');
2224
jest.resetModules();
2325
jest.mock('shared/ReactFeatureFlags', () =>
2426
require('shared/forks/ReactFeatureFlags.native-oss'),
@@ -49,4 +51,24 @@ describe('ReactFabric', () => {
4951
let handle = ReactNative.findNodeHandle(ref.current);
5052
expect(handle).toBe(2);
5153
});
54+
55+
it('sets native props with setNativeProps on Fabric nodes with the RN renderer', () => {
56+
UIManager.updateView.mockReset();
57+
const View = createReactNativeComponentClass('RCTView', () => ({
58+
validAttributes: {title: true},
59+
uiViewClassName: 'RCTView',
60+
}));
61+
62+
let ref = React.createRef();
63+
64+
ReactFabric.render(<View title="bar" ref={ref} />, 11);
65+
expect(UIManager.updateView).not.toBeCalled();
66+
ReactNative.setNativeProps(ref.current, {title: 'baz'});
67+
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
68+
expect(UIManager.updateView).toHaveBeenCalledWith(
69+
expect.any(Number),
70+
'RCTView',
71+
{title: 'baz'},
72+
);
73+
});
5274
});

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,90 @@ describe('ReactNative', () => {
145145
});
146146
});
147147

148+
it('should be able to setNativeProps on native refs', () => {
149+
const View = createReactNativeComponentClass('RCTView', () => ({
150+
validAttributes: {foo: true},
151+
uiViewClassName: 'RCTView',
152+
}));
153+
154+
UIManager.updateView.mockReset();
155+
156+
let viewRef;
157+
ReactNative.render(
158+
<View
159+
foo="bar"
160+
ref={ref => {
161+
viewRef = ref;
162+
}}
163+
/>,
164+
11,
165+
);
166+
167+
expect(UIManager.updateView).not.toBeCalled();
168+
ReactNative.setNativeProps(viewRef, {foo: 'baz'});
169+
expect(UIManager.updateView).toHaveBeenCalledTimes(1);
170+
expect(UIManager.updateView).toHaveBeenCalledWith(
171+
expect.any(Number),
172+
'RCTView',
173+
{foo: 'baz'},
174+
);
175+
});
176+
177+
it('should warn and no-op if calling setNativeProps on non native refs', () => {
178+
const View = createReactNativeComponentClass('RCTView', () => ({
179+
validAttributes: {foo: true},
180+
uiViewClassName: 'RCTView',
181+
}));
182+
183+
class BasicClass extends React.Component {
184+
render() {
185+
return <React.Fragment />;
186+
}
187+
}
188+
189+
class Subclass extends ReactNative.NativeComponent {
190+
render() {
191+
return <View />;
192+
}
193+
}
194+
195+
const CreateClass = createReactClass({
196+
mixins: [NativeMethodsMixin],
197+
render: () => {
198+
return <View />;
199+
},
200+
});
201+
202+
[BasicClass, Subclass, CreateClass].forEach(Component => {
203+
UIManager.updateView.mockReset();
204+
205+
let viewRef;
206+
ReactNative.render(
207+
<Component
208+
foo="bar"
209+
ref={ref => {
210+
viewRef = ref;
211+
}}
212+
/>,
213+
11,
214+
);
215+
216+
expect(UIManager.updateView).not.toBeCalled();
217+
expect(() => {
218+
ReactNative.setNativeProps(viewRef, {foo: 'baz'});
219+
}).toWarnDev(
220+
[
221+
"Warning: setNativeProps was called on a ref that isn't a " +
222+
'native component. Use React.forwardRef to get access ' +
223+
'to the underlying native component',
224+
],
225+
{withoutStack: true},
226+
);
227+
228+
expect(UIManager.updateView).not.toBeCalled();
229+
});
230+
});
231+
148232
it('returns the correct instance and calls it in the callback', () => {
149233
const View = createReactNativeComponentClass('RCTView', () => ({
150234
validAttributes: {foo: true},

0 commit comments

Comments
 (0)