@samitouri / QOS-React / commits / 8bd5b489b4

Defer reading native view config from UIManager until view is used (#10569)

* ReactNative supports lazy view registration Don't react viewConfig from UIManager when a view is registered, but only when it is used for the first time. This will help unblock Prepack optimizations for RN views. * Added new lazy-creation method for native views Also exposed the native view config registry to allow invalid/unsupported host components to fall back to RCTView * Removed ReactNativeViewConfigRegistry from RN __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED After discussion with Spencer Ahrens and Andy Street, it seems the early fallback behavior was mostly for the early days of Ads Manager when Android didn't yet support a lot of the view types. It should be okay now to use an invarient/redbox instead for unsupported views. * Removed non-lazy createReactNativeComponentClass impl There are only a handful of components with JavaScript-defined view configs. It's probably worth making them use the new lazy/async interface as well to be more consistent.

Brian Vaughn committed Aug 31, 2017 at 13:42 UTC 8bd5b489b4cfc080aca8f93e5487abbeb7fc879d
11 files changed +96 -65
src/renderers/native/NativeMethodsMixin.js
+1 -3
@@ -30,10 +30,8 @@ import type {
30 MeasureLayoutOnSuccessCallback,
31 MeasureOnSuccessCallback,
32 NativeMethodsMixinType,
33 -} from 'ReactNativeTypes';
34 -import type {
33 ReactNativeBaseComponentViewConfig,
36 -} from 'ReactNativeViewConfigRegistry';
34 +} from 'ReactNativeTypes';
35
36 /**
37 * `NativeMethodsMixin` provides methods to access the underlying native
src/renderers/native/ReactNativeComponent.js
+1 -3
@@ -28,10 +28,8 @@ import type {
28 MeasureLayoutOnSuccessCallback,
29 MeasureOnSuccessCallback,
30 NativeMethodsMixinType,
31 -} from 'ReactNativeTypes';
32 -import type {
31 ReactNativeBaseComponentViewConfig,
34 -} from 'ReactNativeViewConfigRegistry';
32 +} from 'ReactNativeTypes';
33
34 /**
35 * Superclass that provides methods to access the underlying native component.
src/renderers/native/ReactNativeFiberEntry.js
+1 -1
@@ -100,7 +100,7 @@ const ReactNativeFiber: ReactNativeType = {
100 ReactNativeComponentTree: require('ReactNativeComponentTree'), // InspectorUtils, ScrollResponder
101 ReactNativePropRegistry: require('ReactNativePropRegistry'), // flattenStyle, Stylesheet
102 TouchHistoryMath: require('TouchHistoryMath'), // PanResponder
103 - createReactNativeComponentClass: require('createReactNativeComponentClass'), // eg Text
103 + createReactNativeComponentClass: require('createReactNativeComponentClass'), // eg RCTText, RCTView, ReactNativeART
104 takeSnapshot: require('takeSnapshot'), // react-native-implementation
105 },
106 };
src/renderers/native/ReactNativeFiberHostComponent.js
+1 -3
@@ -24,11 +24,9 @@ import type {
24 MeasureLayoutOnSuccessCallback,
25 MeasureOnSuccessCallback,
26 NativeMethodsMixinType,
27 + ReactNativeBaseComponentViewConfig,
28 } from 'ReactNativeTypes';
29 import type {Instance} from 'ReactNativeFiberRenderer';
29 -import type {
30 - ReactNativeBaseComponentViewConfig,
31 -} from 'ReactNativeViewConfigRegistry';
30
31 /**
32 * This component defines the same methods as NativeMethodsMixin but without the
src/renderers/native/ReactNativeFiberRenderer.js
+1 -3
@@ -24,9 +24,7 @@ const deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationI
24 const emptyObject = require('fbjs/lib/emptyObject');
25 const invariant = require('fbjs/lib/invariant');
26
27 -import type {
28 - ReactNativeBaseComponentViewConfig,
29 -} from 'ReactNativeViewConfigRegistry';
27 +import type {ReactNativeBaseComponentViewConfig} from 'ReactNativeTypes';
28
29 export type Container = number;
30 export type Instance = {
src/renderers/native/ReactNativeTypes.js
+10 -7
@@ -34,6 +34,14 @@ export type MeasureLayoutOnSuccessCallback = (
34 height: number,
35 ) => void;
36
37 +export type ReactNativeBaseComponentViewConfig = {
38 + validAttributes: Object,
39 + uiViewClassName: string,
40 + propTypes?: Object,
41 +};
42 +
43 +export type ViewConfigGetter = () => ReactNativeBaseComponentViewConfig;
44 +
45 /**
46 * This type keeps ReactNativeFiberHostComponent and NativeMethodsMixin in sync.
47 * It can also provide types for ReactNative applications that use NMM or refs.
@@ -51,16 +59,11 @@ export type NativeMethodsMixinType = {
59 setNativeProps(nativeProps: Object): void,
60 };
61
54 -type ReactNativeBaseComponentViewConfig = {
55 - validAttributes: Object,
56 - uiViewClassName: string,
57 - propTypes?: Object,
58 -};
59 -
62 type SecretInternalsType = {
63 NativeMethodsMixin: NativeMethodsMixinType,
64 createReactNativeComponentClass(
63 - viewConfig: ReactNativeBaseComponentViewConfig,
65 + name: string,
66 + callback: ViewConfigGetter,
67 ): any,
68 ReactNativeComponentTree: any,
69 ReactNativePropRegistry: any,
src/renderers/native/ReactNativeViewConfigRegistry.js
+37 -13
@@ -14,29 +14,53 @@
14
15 const invariant = require('fbjs/lib/invariant');
16
17 -export type ReactNativeBaseComponentViewConfig = {
18 - validAttributes: Object,
19 - uiViewClassName: string,
20 - propTypes?: Object,
21 -};
17 +import type {
18 + ReactNativeBaseComponentViewConfig,
19 + ViewConfigGetter,
20 +} from 'ReactNativeTypes';
21
22 +const viewConfigCallbacks = new Map();
23 const viewConfigs = new Map();
24
25 const ReactNativeViewConfigRegistry = {
26 - register(viewConfig: ReactNativeBaseComponentViewConfig) {
27 - const name = viewConfig.uiViewClassName;
26 + /**
27 + * Registers a native view/component by name.
28 + * A callback is provided to load the view config from UIManager.
29 + * The callback is deferred until the view is actually rendered.
30 + * This is done to avoid causing Prepack deopts.
31 + */
32 + register(name: string, callback: ViewConfigGetter): string {
33 invariant(
29 - !viewConfigs.has(name),
34 + !viewConfigCallbacks.has(name),
35 'Tried to register two views with the same name %s',
36 name,
37 );
33 - viewConfigs.set(name, viewConfig);
38 + viewConfigCallbacks.set(name, callback);
39 return name;
40 },
36 - get(name: string) {
37 - const config = viewConfigs.get(name);
38 - invariant(config, 'View config not found for name %s', name);
39 - return config;
41 +
42 + /**
43 + * Retrieves a config for the specified view.
44 + * If this is the first time the view has been used,
45 + * This configuration will be lazy-loaded from UIManager.
46 + */
47 + get(name: string): ReactNativeBaseComponentViewConfig {
48 + let viewConfig;
49 + if (!viewConfigs.has(name)) {
50 + const callback = viewConfigCallbacks.get(name);
51 + invariant(
52 + typeof callback === 'function',
53 + 'View config not found for name %s',
54 + name,
55 + );
56 + viewConfigCallbacks.set(name, null);
57 + viewConfig = callback();
58 + viewConfigs.set(name, viewConfig);
59 + } else {
60 + viewConfig = viewConfigs.get(name);
61 + }
62 + invariant(viewConfig, 'View config not found for name %s', name);
63 + return viewConfig;
64 },
65 };
66
src/renderers/native/__tests__/ReactNativeEvents-test.js
+8 -8
@@ -34,10 +34,10 @@ beforeEach(() => {
34 it('handles events', () => {
35 expect(RCTEventEmitter.register.mock.calls.length).toBe(1);
36 var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
37 - var View = createReactNativeComponentClass({
37 + var View = createReactNativeComponentClass('View', () => ({
38 validAttributes: {foo: true},
39 uiViewClassName: 'View',
40 - });
40 + }));
41
42 var log = [];
43 ReactNative.render(
@@ -94,10 +94,10 @@ it('handles events on text nodes', () => {
94 expect(RCTEventEmitter.register.mock.calls.length).toBe(1);
95 var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
96
97 - var Text = createReactNativeComponentClass({
97 + var Text = createReactNativeComponentClass('Text', () => ({
98 validAttributes: {foo: true},
99 uiViewClassName: 'Text',
100 - });
100 + }));
101
102 class ContextHack extends React.Component {
103 static childContextTypes = {isInAParentText: PropTypes.bool};
@@ -179,10 +179,10 @@ it('handles events on text nodes', () => {
179
180 it('handles when a responder is unmounted while a touch sequence is in progress', () => {
181 var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
182 - var View = createReactNativeComponentClass({
182 + var View = createReactNativeComponentClass('View', () => ({
183 validAttributes: {id: true},
184 uiViewClassName: 'View',
185 - });
185 + }));
186
187 function getViewById(id) {
188 return UIManager.createView.mock.calls.find(
@@ -273,10 +273,10 @@ it('handles when a responder is unmounted while a touch sequence is in progress'
273
274 it('handles events without target', () => {
275 var EventEmitter = RCTEventEmitter.register.mock.calls[0][0];
276 - var View = createReactNativeComponentClass({
276 + var View = createReactNativeComponentClass('View', () => ({
277 validAttributes: {id: true},
278 uiViewClassName: 'View',
279 - });
279 + }));
280
281 function getViewById(id) {
282 return UIManager.createView.mock.calls.find(
src/renderers/native/__tests__/ReactNativeMount-test.js
+12 -12
@@ -27,10 +27,10 @@ describe('ReactNative', () => {
27 });
28
29 it('should be able to create and render a native component', () => {
30 - var View = createReactNativeComponentClass({
30 + var View = createReactNativeComponentClass('View', () => ({
31 validAttributes: {foo: true},
32 uiViewClassName: 'View',
33 - });
33 + }));
34
35 ReactNative.render(<View foo="test" />, 1);
36 expect(UIManager.createView).toBeCalled();
@@ -40,10 +40,10 @@ describe('ReactNative', () => {
40 });
41
42 it('should be able to create and update a native component', () => {
43 - var View = createReactNativeComponentClass({
43 + var View = createReactNativeComponentClass('View', () => ({
44 validAttributes: {foo: true},
45 uiViewClassName: 'View',
46 - });
46 + }));
47
48 ReactNative.render(<View foo="foo" />, 11);
49
@@ -61,10 +61,10 @@ describe('ReactNative', () => {
61 });
62
63 it('should not call UIManager.updateView after render for properties that have not changed', () => {
64 - const Text = createReactNativeComponentClass({
64 + const Text = createReactNativeComponentClass('Text', () => ({
65 validAttributes: {foo: true},
66 uiViewClassName: 'Text',
67 - });
67 + }));
68
69 ReactNative.render(<Text foo="a">1</Text>, 11);
70 expect(UIManager.updateView).not.toBeCalled();
@@ -87,10 +87,10 @@ describe('ReactNative', () => {
87 });
88
89 it('should not call UIManager.updateView from setNativeProps for properties that have not changed', () => {
90 - const View = createReactNativeComponentClass({
90 + const View = createReactNativeComponentClass('View', () => ({
91 validAttributes: {foo: true},
92 uiViewClassName: 'View',
93 - });
93 + }));
94
95 class Subclass extends ReactNative.NativeComponent {
96 render() {
@@ -122,10 +122,10 @@ describe('ReactNative', () => {
122 });
123
124 it('returns the correct instance and calls it in the callback', () => {
125 - var View = createReactNativeComponentClass({
125 + var View = createReactNativeComponentClass('View', () => ({
126 validAttributes: {foo: true},
127 uiViewClassName: 'View',
128 - });
128 + }));
129
130 var a;
131 var b;
@@ -143,10 +143,10 @@ describe('ReactNative', () => {
143 });
144
145 it('renders and reorders children', () => {
146 - var View = createReactNativeComponentClass({
146 + var View = createReactNativeComponentClass('View', () => ({
147 validAttributes: {title: true},
148 uiViewClassName: 'View',
149 - });
149 + }));
150
151 class Component extends React.Component {
152 render() {
src/renderers/native/__tests__/createReactNativeComponentClass-test.js
+16 -4
@@ -34,8 +34,14 @@ describe('createReactNativeComponentClass', () => {
34 uiViewClassName: 'View',
35 };
36
37 - const Text = createReactNativeComponentClass(textViewConfig);
38 - const View = createReactNativeComponentClass(viewViewConfig);
37 + const Text = createReactNativeComponentClass(
38 + textViewConfig.uiViewClassName,
39 + () => textViewConfig,
40 + );
41 + const View = createReactNativeComponentClass(
42 + viewViewConfig.uiViewClassName,
43 + () => viewViewConfig,
44 + );
45
46 expect(Text).not.toBe(View);
47
@@ -53,10 +59,16 @@ describe('createReactNativeComponentClass', () => {
59 uiViewClassName: 'Text', // Same
60 };
61
56 - createReactNativeComponentClass(textViewConfig);
62 + createReactNativeComponentClass(
63 + textViewConfig.uiViewClassName,
64 + () => textViewConfig,
65 + );
66
67 expect(() => {
59 - createReactNativeComponentClass(altTextViewConfig);
68 + createReactNativeComponentClass(
69 + altTextViewConfig.uiViewClassName,
70 + () => altTextViewConfig,
71 + );
72 }).toThrow('Tried to register two views with the same name Text');
73 });
74 });
src/renderers/native/createReactNativeComponentClass.js
+8 -8
@@ -14,21 +14,21 @@
14
15 const ReactNativeViewConfigRegistry = require('ReactNativeViewConfigRegistry');
16
17 -// See also ReactNativeBaseComponent
18 -export type ReactNativeBaseComponentViewConfig = {
19 - validAttributes: Object,
20 - uiViewClassName: string,
21 - propTypes?: Object,
22 -};
17 +import type {ViewConfigGetter} from 'ReactNativeTypes';
18
19 /**
20 + * Creates a renderable ReactNative host component.
21 + * Use this method for view configs that are loaded from UIManager.
22 + * Use createReactNativeComponentClass() for view configs defined within JavaScript.
23 + *
24 * @param {string} config iOS View configuration.
25 * @private
26 */
27 const createReactNativeComponentClass = function(
29 - viewConfig: ReactNativeBaseComponentViewConfig,
28 + name: string,
29 + callback: ViewConfigGetter,
30 ): string {
31 - return ReactNativeViewConfigRegistry.register(viewConfig);
31 + return ReactNativeViewConfigRegistry.register(name, callback);
32 };
33
34 module.exports = createReactNativeComponentClass;