Skip to content

Commit

Permalink
Introduce flow type to differentiate between HostComponent, NativeMet…
Browse files Browse the repository at this point in the history
…hodsMixin, 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
  • Loading branch information
elicwhite authored and facebook-github-bot committed Sep 25, 2019
1 parent 94e8ccf commit 69c38e5
Show file tree
Hide file tree
Showing 13 changed files with 109 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> = requireNativeComponent<mixed>(
'RCTRefreshControl',
);

class RefreshControlMock extends React.Component<{}> {
static latestRef: ?RefreshControlMock;
componentDidMount() {
RefreshControlMock.latestRef = this;
}
render(): React.Element<string> {
render(): React.Element<typeof RCTRefreshControl> {
return <RCTRefreshControl />;
}
}
Expand Down
2 changes: 2 additions & 0 deletions Libraries/Components/ScrollResponder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -543,6 +544,7 @@ const ScrollResponderMixin = {
scrollResponderScrollNativeHandleToKeyboard: function<T>(
nodeHandle:
| number
| React.ElementRef<HostComponent<T>>
| React.ElementRef<Class<ReactNative.NativeComponent<T>>>,
additionalOffset?: number,
preventNegativeScrollOffset?: boolean,
Expand Down
8 changes: 6 additions & 2 deletions Libraries/Components/ScrollView/__mocks__/ScrollViewMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> = requireNativeComponent<mixed>(
'RCTScrollView',
);

const ScrollViewComponent: $FlowFixMe = jest.genMockFromModule('../ScrollView');

class ScrollViewMock extends ScrollViewComponent {
render(): React.Element<string> {
render(): React.Element<typeof RCTScrollView> {
return (
<RCTScrollView {...this.props}>
{this.props.refreshControl}
Expand Down
9 changes: 3 additions & 6 deletions Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -904,9 +905,7 @@ const TextInput = createReactClass({
this._inputRef = ref;
},

getNativeRef: function(): ?React.ElementRef<
Class<ReactNative.NativeComponent<mixed>>,
> {
getNativeRef: function(): ?React.ElementRef<HostComponent<mixed>> {
return this._inputRef;
},

Expand Down Expand Up @@ -1230,9 +1229,7 @@ const TextInput = createReactClass({
class InternalTextInputType extends ReactNative.NativeComponent<Props> {
clear() {}

getNativeRef(): ?React.ElementRef<
Class<ReactNative.NativeComponent<mixed>>,
> {}
getNativeRef(): ?React.ElementRef<HostComponent<mixed>> {}

// $FlowFixMe
isFocused(): boolean {}
Expand Down
6 changes: 2 additions & 4 deletions Libraries/Components/View/ViewNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<ViewProps>,
>;
export type ViewNativeComponentType = HostComponent<ViewProps>;

let NativeViewComponent;
let viewConfig:
Expand Down
40 changes: 22 additions & 18 deletions Libraries/Image/Image.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,15 @@ async function queryCache(
return await ImageLoader.queryCache(urls);
}

declare class ImageComponentType extends ReactNative.NativeComponent<ImagePropsType> {
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,
Expand All @@ -224,10 +224,7 @@ declare class ImageComponentType extends ReactNative.NativeComponent<ImagePropsT
*
* See https://facebook.github.io/react-native/docs/image.html
*/
let Image = (
props: ImagePropsType,
forwardedRef: ?React.Ref<'RCTTextInlineImage' | 'ImageViewNativeComponent'>,
) => {
let Image = (props: ImagePropsType, forwardedRef) => {
let source = resolveAssetSource(props.source);
const defaultSource = resolveAssetSource(props.defaultSource);
const loadingIndicatorSource = resolveAssetSource(
Expand Down Expand Up @@ -303,7 +300,12 @@ let Image = (
);
};

Image = React.forwardRef(Image);
Image = React.forwardRef<
ImagePropsType,
| React.ElementRef<typeof TextInlineImageNativeComponent>
| React.ElementRef<typeof ImageViewNativeComponent>,
>(Image);

Image.displayName = 'Image';

/**
Expand Down Expand Up @@ -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<ImageComponentType>);
module.exports = ((Image: any): React.AbstractComponent<
ImagePropsType,
| React.ElementRef<typeof TextInlineImageNativeComponent>
| React.ElementRef<typeof ImageViewNativeComponent>,
> &
ImageComponentStatics);
45 changes: 23 additions & 22 deletions Libraries/Image/Image.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> = requireNativeComponent(
'RCTImageView',
);

function getSize(
uri: string,
success: (width: number, height: number) => void,
Expand Down Expand Up @@ -70,14 +71,14 @@ async function queryCache(
return await ImageViewManager.queryCache(urls);
}

declare class ImageComponentType extends ReactNative.NativeComponent<ImagePropsType> {
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,
Expand All @@ -86,10 +87,7 @@ declare class ImageComponentType extends ReactNative.NativeComponent<ImagePropsT
*
* See https://facebook.github.io/react-native/docs/image.html
*/
let Image = (
props: ImagePropsType,
forwardedRef: ?React.Ref<'RCTImageView'>,
) => {
let Image = (props: ImagePropsType, forwardedRef) => {
const source = resolveAssetSource(props.source) || {
uri: undefined,
width: undefined,
Expand Down Expand Up @@ -140,7 +138,9 @@ let Image = (
);
};

Image = React.forwardRef(Image);
Image = React.forwardRef<ImagePropsType, React.ElementRef<typeof RCTImageView>>(
Image,
);
Image.displayName = 'Image';

/**
Expand Down Expand Up @@ -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<ImageComponentType>);
module.exports = ((Image: any): React.AbstractComponent<
ImagePropsType,
React.ElementRef<typeof RCTImageView>,
> &
ImageComponentStatics);
8 changes: 6 additions & 2 deletions Libraries/Image/ImageViewNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> = requireNativeComponent<mixed>(
'RCTImageView',
);

module.exports = ImageViewNativeComponent;
7 changes: 5 additions & 2 deletions Libraries/Image/TextInlineImageNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<mixed> = requireNativeComponent<mixed>(
'RCTTextInlineImage',
);

module.exports = TextInlineImage;
11 changes: 7 additions & 4 deletions Libraries/ReactNative/requireNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

Expand All @@ -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
Expand All @@ -21,9 +23,10 @@ const getNativeComponentAttributes = require('./getNativeComponentAttributes');
* const View = requireNativeComponent('RCTView');
*
*/
const requireNativeComponent = (uiViewClassName: string): string =>
createReactNativeComponentClass(uiViewClassName, () =>

const requireNativeComponent = <T>(uiViewClassName: string): HostComponent<T> =>
((createReactNativeComponentClass(uiViewClassName, () =>
getNativeComponentAttributes(uiViewClassName),
);
): any): HostComponent<T>);

module.exports = requireNativeComponent;
18 changes: 17 additions & 1 deletion Libraries/Renderer/shims/ReactNativeTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @flow
*/

import React from 'react';
import * as React from 'react';

export type MeasureOnSuccessCallback = (
x: number,
Expand Down Expand Up @@ -113,6 +113,22 @@ export type NativeMethodsMixinType = {
setNativeProps(nativeProps: Object): void,
};

export type HostComponent<T> = 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,
Expand Down
10 changes: 5 additions & 5 deletions Libraries/Utilities/codegenNativeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,7 +22,7 @@ type Options = $ReadOnly<{|
paperComponentNameDeprecated?: string,
|}>;

export type NativeComponentType<T> = Class<NativeComponent<T>>;
export type NativeComponentType<T> = HostComponent<T>;

function codegenNativeComponent<Props>(
componentName: string,
Expand Down Expand Up @@ -53,9 +53,9 @@ function codegenNativeComponent<Props>(
// 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<Props>,
>);
return (requireNativeComponent<Props>(
componentNameInUse,
): HostComponent<Props>);
}

export default codegenNativeComponent;
Loading

0 comments on commit 69c38e5

Please sign in to comment.