From 69c38e5a639f34620038ae5724426c92c355e509 Mon Sep 17 00:00:00 2001 From: Eli White Date: Wed, 25 Sep 2019 10:10:48 -0700 Subject: [PATCH] Introduce flow type to differentiate between HostComponent, NativeMethodsMixin, and NativeComponent Summary: In React Native there are three types of "Native" components. ``` createReactClass with NativeMethodsMixin ``` ``` class MyComponent extends ReactNative.NativeComponent ``` ``` requireNativeComponent('RCTView') ``` The implementation for how to handle all three of these exists in the React Native Renderer. Refs attached to components created via these methods provide a set of functions such as ``` .measure .measureInWindow .measureLayout .setNativeProps ``` These methods have been used for our core components in the repo to provide a consistent API. Many of the APIs in React Native require a `reactTag` to a host component. This is acquired by calling `findNodeHandle` with any component. `findNodeHandle` works with the first two approaches. For a lot of our new Fabric APIs, we will require passing a ref to a HostComponent directly instead of relying on `findNodeHandle` to tunnel through the component tree as that behavior isn't safe with React concurrent mode. The goal of this change is to enable us to differentiate between components created with `requireNativeComponent` and the other types. This will be needed to be able to safely type the new APIs. For existing components that should support being a host component but need to use some JS behavior in a wrapper, they should use `forwardRef`. The majority of React Native's core components were migrated to use `forwardRef` last year. Components that can't use forwardRef will need to have a method like `getNativeRef()` to get access to the underlying host component ref. Note, we will need follow up changes as well as changes to the React Renderer in the React repo to fully utilize this new type. Changelog: [Internal] Flow type to differentiate between HostComponent and NativeMethodsMixin and NativeComponent Reviewed By: jbrown215 Differential Revision: D17551089 fbshipit-source-id: 7a30b4bb4323156c0b2465ca41fcd05f4315becf --- .../__mocks__/RefreshControlMock.js | 8 +++- Libraries/Components/ScrollResponder.js | 2 + .../ScrollView/__mocks__/ScrollViewMock.js | 8 +++- Libraries/Components/TextInput/TextInput.js | 9 ++-- .../Components/View/ViewNativeComponent.js | 6 +-- Libraries/Image/Image.android.js | 40 +++++++++-------- Libraries/Image/Image.ios.js | 45 ++++++++++--------- Libraries/Image/ImageViewNativeComponent.js | 8 +++- .../Image/TextInlineImageNativeComponent.js | 7 ++- .../ReactNative/requireNativeComponent.js | 11 +++-- Libraries/Renderer/shims/ReactNativeTypes.js | 18 +++++++- Libraries/Utilities/codegenNativeComponent.js | 10 ++--- index.js | 6 ++- 13 files changed, 109 insertions(+), 69 deletions(-) diff --git a/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js b/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js index 1ca76f7f2d17b3..46e91d97c59836 100644 --- a/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js +++ b/Libraries/Components/RefreshControl/__mocks__/RefreshControlMock.js @@ -14,14 +14,18 @@ const React = require('react'); const requireNativeComponent = require('../../../ReactNative/requireNativeComponent'); -const RCTRefreshControl = requireNativeComponent('RCTRefreshControl'); +import type {HostComponent} from '../../../Renderer/shims/ReactNativeTypes'; + +const RCTRefreshControl: HostComponent = requireNativeComponent( + 'RCTRefreshControl', +); class RefreshControlMock extends React.Component<{}> { static latestRef: ?RefreshControlMock; componentDidMount() { RefreshControlMock.latestRef = this; } - render(): React.Element { + render(): React.Element { return ; } } diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 37541a30711f99..f7da4efeae86cc 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -28,6 +28,7 @@ import type {PressEvent, ScrollEvent} from '../Types/CoreEventTypes'; import type {Props as ScrollViewProps} from './ScrollView/ScrollView'; import type {KeyboardEvent} from './Keyboard/Keyboard'; import type EmitterSubscription from '../vendor/emitter/EmitterSubscription'; +import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; /** * Mixin that can be integrated in order to handle scrolling that plays well @@ -543,6 +544,7 @@ const ScrollResponderMixin = { scrollResponderScrollNativeHandleToKeyboard: function( nodeHandle: | number + | React.ElementRef> | React.ElementRef>>, additionalOffset?: number, preventNegativeScrollOffset?: boolean, diff --git a/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js b/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js index 7360b7778dcaff..30e8cee004192b 100644 --- a/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js +++ b/Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js @@ -17,12 +17,16 @@ const View = require('../../View/View'); const requireNativeComponent = require('../../../ReactNative/requireNativeComponent'); -const RCTScrollView = requireNativeComponent('RCTScrollView'); +import type {HostComponent} from '../../../Renderer/shims/ReactNativeTypes'; + +const RCTScrollView: HostComponent = requireNativeComponent( + 'RCTScrollView', +); const ScrollViewComponent: $FlowFixMe = jest.genMockFromModule('../ScrollView'); class ScrollViewMock extends ScrollViewComponent { - render(): React.Element { + render(): React.Element { return ( {this.props.refreshControl} diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 9a5532bb2f1237..f38a2d966a3a14 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -32,6 +32,7 @@ import type {ColorValue} from '../../StyleSheet/StyleSheetTypes'; import type {ViewProps} from '../View/ViewPropTypes'; import type {SyntheticEvent, ScrollEvent} from '../../Types/CoreEventTypes'; import type {PressEvent} from '../../Types/CoreEventTypes'; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; let AndroidTextInput; let RCTMultilineTextInputView; @@ -904,9 +905,7 @@ const TextInput = createReactClass({ this._inputRef = ref; }, - getNativeRef: function(): ?React.ElementRef< - Class>, - > { + getNativeRef: function(): ?React.ElementRef> { return this._inputRef; }, @@ -1230,9 +1229,7 @@ const TextInput = createReactClass({ class InternalTextInputType extends ReactNative.NativeComponent { clear() {} - getNativeRef(): ?React.ElementRef< - Class>, - > {} + getNativeRef(): ?React.ElementRef> {} // $FlowFixMe isFocused(): boolean {} diff --git a/Libraries/Components/View/ViewNativeComponent.js b/Libraries/Components/View/ViewNativeComponent.js index c735b5b864a77b..2219912aab7492 100644 --- a/Libraries/Components/View/ViewNativeComponent.js +++ b/Libraries/Components/View/ViewNativeComponent.js @@ -11,17 +11,15 @@ 'use strict'; const Platform = require('../../Utilities/Platform'); -const ReactNative = require('../../Renderer/shims/ReactNative'); const ReactNativeViewViewConfigAndroid = require('./ReactNativeViewViewConfigAndroid'); const registerGeneratedViewConfig = require('../../Utilities/registerGeneratedViewConfig'); const requireNativeComponent = require('../../ReactNative/requireNativeComponent'); import type {ViewProps} from './ViewPropTypes'; +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; -export type ViewNativeComponentType = Class< - ReactNative.NativeComponent, ->; +export type ViewNativeComponentType = HostComponent; let NativeViewComponent; let viewConfig: diff --git a/Libraries/Image/Image.android.js b/Libraries/Image/Image.android.js index 4c9d13d5e5bd4c..76f0af52c05d54 100644 --- a/Libraries/Image/Image.android.js +++ b/Libraries/Image/Image.android.js @@ -207,15 +207,15 @@ async function queryCache( return await ImageLoader.queryCache(urls); } -declare class ImageComponentType extends ReactNative.NativeComponent { - static getSize: typeof getSize; - static getSizeWithHeaders: typeof getSizeWithHeaders; - static prefetch: typeof prefetch; - static abortPrefetch: typeof abortPrefetch; - static queryCache: typeof queryCache; - static resolveAssetSource: typeof resolveAssetSource; - static propTypes: typeof ImageProps; -} +type ImageComponentStatics = $ReadOnly<{| + getSize: typeof getSize, + getSizeWithHeaders: typeof getSizeWithHeaders, + prefetch: typeof prefetch, + abortPrefetch: typeof abortPrefetch, + queryCache: typeof queryCache, + resolveAssetSource: typeof resolveAssetSource, + propTypes: typeof ImageProps, +|}>; /** * A React component for displaying different types of images, @@ -224,10 +224,7 @@ declare class ImageComponentType extends ReactNative.NativeComponent, -) => { +let Image = (props: ImagePropsType, forwardedRef) => { let source = resolveAssetSource(props.source); const defaultSource = resolveAssetSource(props.defaultSource); const loadingIndicatorSource = resolveAssetSource( @@ -303,7 +300,12 @@ let Image = ( ); }; -Image = React.forwardRef(Image); +Image = React.forwardRef< + ImagePropsType, + | React.ElementRef + | React.ElementRef, +>(Image); + Image.displayName = 'Image'; /** @@ -379,7 +381,9 @@ const styles = StyleSheet.create({ }, }); -/* $FlowFixMe(>=0.89.0 site=react_native_android_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ -module.exports = (Image: Class); +module.exports = ((Image: any): React.AbstractComponent< + ImagePropsType, + | React.ElementRef + | React.ElementRef, +> & + ImageComponentStatics); diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index e92ef67b336299..3c66023c5af005 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -19,14 +19,15 @@ const flattenStyle = require('../StyleSheet/flattenStyle'); const requireNativeComponent = require('../ReactNative/requireNativeComponent'); const resolveAssetSource = require('./resolveAssetSource'); -const ImageViewManager = NativeModules.ImageViewManager; - -const RCTImageView = requireNativeComponent('RCTImageView'); - import type {ImageProps as ImagePropsType} from './ImageProps'; - +import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; import type {ImageStyleProp} from '../StyleSheet/StyleSheet'; +const ImageViewManager = NativeModules.ImageViewManager; +const RCTImageView: HostComponent = requireNativeComponent( + 'RCTImageView', +); + function getSize( uri: string, success: (width: number, height: number) => void, @@ -70,14 +71,14 @@ async function queryCache( return await ImageViewManager.queryCache(urls); } -declare class ImageComponentType extends ReactNative.NativeComponent { - static getSize: typeof getSize; - static getSizeWithHeaders: typeof getSizeWithHeaders; - static prefetch: typeof prefetch; - static queryCache: typeof queryCache; - static resolveAssetSource: typeof resolveAssetSource; - static propTypes: typeof DeprecatedImagePropType; -} +type ImageComponentStatics = $ReadOnly<{| + getSize: typeof getSize, + getSizeWithHeaders: typeof getSizeWithHeaders, + prefetch: typeof prefetch, + queryCache: typeof queryCache, + resolveAssetSource: typeof resolveAssetSource, + propTypes: typeof DeprecatedImagePropType, +|}>; /** * A React component for displaying different types of images, @@ -86,10 +87,7 @@ declare class ImageComponentType extends ReactNative.NativeComponent, -) => { +let Image = (props: ImagePropsType, forwardedRef) => { const source = resolveAssetSource(props.source) || { uri: undefined, width: undefined, @@ -140,7 +138,9 @@ let Image = ( ); }; -Image = React.forwardRef(Image); +Image = React.forwardRef>( + Image, +); Image.displayName = 'Image'; /** @@ -206,7 +206,8 @@ const styles = StyleSheet.create({ }, }); -/* $FlowFixMe(>=0.89.0 site=react_native_ios_fb) This comment suppresses an - * error found when Flow v0.89 was deployed. To see the error, delete this - * comment and run Flow. */ -module.exports = (Image: Class); +module.exports = ((Image: any): React.AbstractComponent< + ImagePropsType, + React.ElementRef, +> & + ImageComponentStatics); diff --git a/Libraries/Image/ImageViewNativeComponent.js b/Libraries/Image/ImageViewNativeComponent.js index 06722105bdc229..bcafce94d630c2 100644 --- a/Libraries/Image/ImageViewNativeComponent.js +++ b/Libraries/Image/ImageViewNativeComponent.js @@ -5,13 +5,17 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict-local */ 'use strict'; const requireNativeComponent = require('../ReactNative/requireNativeComponent'); -const ImageViewNativeComponent: string = requireNativeComponent('RCTImageView'); +import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; + +const ImageViewNativeComponent: HostComponent = requireNativeComponent( + 'RCTImageView', +); module.exports = ImageViewNativeComponent; diff --git a/Libraries/Image/TextInlineImageNativeComponent.js b/Libraries/Image/TextInlineImageNativeComponent.js index 144e4314f8d645..139c88ba8a17f0 100644 --- a/Libraries/Image/TextInlineImageNativeComponent.js +++ b/Libraries/Image/TextInlineImageNativeComponent.js @@ -5,13 +5,16 @@ * LICENSE file in the root directory of this source tree. * * @format - * @flow + * @flow strict-local */ 'use strict'; const requireNativeComponent = require('../ReactNative/requireNativeComponent'); +import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; -const TextInlineImage: string = requireNativeComponent('RCTTextInlineImage'); +const TextInlineImage: HostComponent = requireNativeComponent( + 'RCTTextInlineImage', +); module.exports = TextInlineImage; diff --git a/Libraries/ReactNative/requireNativeComponent.js b/Libraries/ReactNative/requireNativeComponent.js index a2417e15983198..02d33db9b5ee30 100644 --- a/Libraries/ReactNative/requireNativeComponent.js +++ b/Libraries/ReactNative/requireNativeComponent.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict-local + * @flow * @format */ @@ -13,6 +13,8 @@ const createReactNativeComponentClass = require('../Renderer/shims/createReactNativeComponentClass'); const getNativeComponentAttributes = require('./getNativeComponentAttributes'); +import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; + /** * Creates values that can be used like React components which represent native * view managers. You should create JavaScript modules that wrap these values so @@ -21,9 +23,10 @@ const getNativeComponentAttributes = require('./getNativeComponentAttributes'); * const View = requireNativeComponent('RCTView'); * */ -const requireNativeComponent = (uiViewClassName: string): string => - createReactNativeComponentClass(uiViewClassName, () => + +const requireNativeComponent = (uiViewClassName: string): HostComponent => + ((createReactNativeComponentClass(uiViewClassName, () => getNativeComponentAttributes(uiViewClassName), - ); + ): any): HostComponent); module.exports = requireNativeComponent; diff --git a/Libraries/Renderer/shims/ReactNativeTypes.js b/Libraries/Renderer/shims/ReactNativeTypes.js index 04200f2bb9fa87..99e2941654cdd3 100644 --- a/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/Libraries/Renderer/shims/ReactNativeTypes.js @@ -8,7 +8,7 @@ * @flow */ -import React from 'react'; +import * as React from 'react'; export type MeasureOnSuccessCallback = ( x: number, @@ -113,6 +113,22 @@ export type NativeMethodsMixinType = { setNativeProps(nativeProps: Object): void, }; +export type HostComponent = React.AbstractComponent< + T, + $ReadOnly<{| + blur(): void, + focus(): void, + measure(callback: MeasureOnSuccessCallback): void, + measureInWindow(callback: MeasureInWindowOnSuccessCallback): void, + measureLayout( + relativeToNativeNode: number | Object, + onSuccess: MeasureLayoutOnSuccessCallback, + onFail?: () => void, + ): void, + setNativeProps(nativeProps: Object): void, + |}>, +>; + type SecretInternalsType = { NativeMethodsMixin: NativeMethodsMixinType, computeComponentStackForErrorReporting(tag: number): string, diff --git a/Libraries/Utilities/codegenNativeComponent.js b/Libraries/Utilities/codegenNativeComponent.js index 7a39d7a65d40fe..eda844a78a46d8 100644 --- a/Libraries/Utilities/codegenNativeComponent.js +++ b/Libraries/Utilities/codegenNativeComponent.js @@ -11,8 +11,8 @@ 'use strict'; -import type {NativeComponent} from '../../Libraries/Renderer/shims/ReactNative'; import requireNativeComponent from '../../Libraries/ReactNative/requireNativeComponent'; +import type {HostComponent} from '../../Libraries/Renderer/shims/ReactNativeTypes'; import {UIManager} from 'react-native'; // TODO: import from CodegenSchema once workspaces are enabled @@ -22,7 +22,7 @@ type Options = $ReadOnly<{| paperComponentNameDeprecated?: string, |}>; -export type NativeComponentType = Class>; +export type NativeComponentType = HostComponent; function codegenNativeComponent( componentName: string, @@ -53,9 +53,9 @@ function codegenNativeComponent( // generated with the view config babel plugin, so we need to require the native component. // // This will be useful during migration, but eventually this will error. - return ((requireNativeComponent(componentNameInUse): any): Class< - NativeComponent, - >); + return (requireNativeComponent( + componentNameInUse, + ): HostComponent); } export default codegenNativeComponent; diff --git a/index.js b/index.js index 5dac3191ecc2f8..8e7c6281b3a3ec 100644 --- a/index.js +++ b/index.js @@ -99,6 +99,8 @@ import typeof DeprecatedEdgeInsetsPropType from './Libraries/DeprecatedPropTypes import typeof DeprecatedPointPropType from './Libraries/DeprecatedPropTypes/DeprecatedPointPropType'; import typeof DeprecatedViewPropTypes from './Libraries/DeprecatedPropTypes/DeprecatedViewPropTypes'; +import type {HostComponent} from './Libraries/Renderer/shims/ReactNativeTypes'; + const invariant = require('invariant'); const warnOnce = require('./Libraries/Utilities/warnOnce'); @@ -427,7 +429,9 @@ module.exports = { get processColor(): processColor { return require('./Libraries/StyleSheet/processColor'); }, - get requireNativeComponent(): requireNativeComponent { + get requireNativeComponent(): ( + uiViewClassName: string, + ) => HostComponent { return require('./Libraries/ReactNative/requireNativeComponent'); }, get unstable_RootTagContext(): RootTagContext {