React-native: πŸ‘» Flow Typing NativeModules

Created on 16 May 2019  Β·  66Comments  Β·  Source: facebook/react-native

Flow Typing NativeModules

The TurboModule system is nearly feature-complete on both Android and iOS. Before we rollout the system, we'd like to migrate all our OSS NativeModules to use it. As a prerequisite for this migration, all the the NativeModules must have spec files. There are currently 41 NativeModules in total, so there is a bunch of work to do. We'd love to use this as an opportunity to help get more people involved by contributing to React Native.

Instructions

  1. Pick an unclaimed module and comment that you're working on it.
  2. Create a Spec file called NativeXYZ.js, where XYZ is the name of the NativeModule.
  3. Define flow types based on the NativeModule's native methods.
  4. Change call-sites from NativeModules.XYZ to NativeXYZ
  5. Make sure flow passes
  6. Do one module per PR

What is a Spec file?

A Spec file looks like this:

NativeAnalytics.js

/**
 * Copyright 2004-present Facebook. All Rights Reserved.
 *
 * @flow strict-local
 * @format
 */

'use strict';

import type {TurboModule} from 'RCTExport';
import * as TurboModuleRegistry from 'TurboModuleRegistry';

export interface Spec extends TurboModule {
  +getConstants: () => {|
    constant1: string,
    constant2: boolean,
  |};
  +logCounter: (key: string, value: number) => void;
  +logEvent: (eventName: string, data: Object, analyticsModule: ?string) => void;
  +logRealtimeEvent: (
    eventName: string,
    data: Object,
    analyticsModule: ?string,
  ) => void;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
  'Analytics',
);

Observations

  • We use TurboModuleRegistry.getEnforcing<Spec>('Analytics') as opposed to NativeModules.Analytics to require the NativeModule. TurboModuleRegistry.getEnforcing is an indirection that allows us to require both NativeModules and TurboModules. It throws if the NativeModule isn't there. In the case that a NativeModule is platform-specific, please use Platform.OS and conditionally call TurboModuleRegistry.getEnforcing.
  • Each Spec file exports an interface called Spec that extends TurboModule. This interface contains typings for methods exposed by the NativeModule/TurboModule.
  • NativeModule constants are declared in the return type of the getConstants() method. The old way of accessing constants as properties on NativeModule objects is deprecated and unsupported by the TurboModule system. On iOS, these constants map to the return type of constantsToExport in iOS and getConstants in Android.
  • Each Spec file is named Native*.js. The filename matters because our codegen uses it to name the generated Java interfaces, ObjC protocols, and C++ TurboModule subclasses. NativeAnalytics.js, for instance, will generate a Java interface and ObjC protocol called NativeAnalyticsSpec, and a C++ TurboModule class called NativeAnalyticsSpecJSI. After these Spec files are typed, we'll generate and check in the codegen output into this repository. Then, after we open source our codegen, we'll delete the generated interfaces, protocols, and C++ classes from this repository.

    • Note that the naming convention used here is not finalized, but stable enough for the time being.

  • This isn't a ViewManager. We're currently not focusing on ViewManagers. Therefore, there's no need to write spec files for them.
  • We're not using imported types. Our codegen doesn't support imported types. So, for the time being, all types used by the spec must be declared in the same file.

Supported Types

  • Methods with simple args:

    • string

    • boolean

    • number

    • Object

    • Array<*>*

  • Function and typed function:

    • Methods with nullable args

    • ?string

    • ?Object

    • ?Array<*>*

    • ?Function and nullable typed function

    • number and boolean are not supported

  • Methods with inline complex Object typed args

    • e.g. (x: {|foo: string, ... |}) => void

  • Methods that return a Promise<*>*
  • Synchronous methods that return simple non-nullable types

    • string

    • number

    • boolean

    • Object

    • Array<*>*

  • Optional methods with all variants above
  • Typed exported constants
  • Note: Flow unions aren't supported

How do you type NativeModules?

You have to deduce the types by looking at the NativeModule implementations on iOS and Android. Some NativeModules already have JS wrappers, so you could also look at those for writing the Spec files.

Guidelines

  • How do you know which methods are exported to JS?

    • On Android: Look for NativeModule non-inherited class methods annotated with @ReactMethod.
    • On iOS: Look for NativeModule non-inherited and inherited class methods wrapped in RCT_EXPORT_*_METHOD macros .
  • How do you know when a method should have a Promise return type?

    • On Android: The last argument of the method is the Promise object.
    • On iOS: The last two arguments are a RCTPromiseResolveBlock and RCTPromiseRejectBlock
  • What should the JS method name be?

    • On Android: It's the name of the Java method.
    • On iOS: It's the part of the selector before the first argument/color.
  • NSDictionary on iOS, and ReadableMap and WritableMap on Android translate to object literals in JS.
  • RCTResponseSenderBlock and RCTResponseErrorBlock on iOS, and Callback on Android translate to function literals in JS.
  • Try to avoid using Function and Object whenever possible.
  • In the case that a method or constant is unavailable on one platform, make it optional.
  • In the case that a NativeModule is unavailable on one platform, make it optional.
  • Where do you put the Spec file?

    • Most NativeModules belong to a Library (see the JS section of the table below), so you could write the Spec in the library. For the other NativeModules, write the spec inside react-native-github/Libraries/NativeModules/specs.

Platform-specific NativeModules

For platform-specific NativeModules, use Platform.OS to conditionally call TurboModuleRegistry.getEnforcing<Spec>. When changing the call-sites of these NativeModules, you may have to guard them with null checks to please flow.

Call-site Before

const NativeModules = require('NativeModules');
const IntentAndroid = NativeModules.IntentAndroid;

function foo() {
  if (Platform.OS === 'android') {
    IntentAndroid.method();
  }
}

Call-site After

const NativeIntentAndroid = require('NativeIntentAndroid');
const {Platform} = require('react-native');

function foo() {
  if (Platform.OS === 'android') {
    if (NativeIntentAndroid != null) {
      NativeIntentAndroid.method();
    }
  }
}

NativeModule List

Please claim a NativeModule from the list below.

⬜️ - Unclaimed
🚧 - Claimed / PR in progress
βœ… - PR Merged

|Status |JS Name |JS NativeModule Wrappers |iOS |Android |
|--- |--- |--- |--- |--- |
| βœ… |AccessibilityInfo |react-native-github/Libraries/Components/AccessibilityInfo/AccessibilityInfo.android.js | |AccessibilityInfoModule.java |
| βœ… |AccessibilityManager |react-native-github/Libraries/Components/AccessibilityInfo/AccessibilityInfo.ios.js |RCTAccessibilityManager.m | |
| βœ… |AlertManager |react-native-github/Libraries/Alert/Alert.js |RCTAlertManager.m | |
| βœ… |AnimationDebugModule | | |AnimationsDebugModule.java |
| βœ… |AppState |react-native-github/Libraries/AppState/AppState.js |RCTAppState.m |AppStateModule.java |
| βœ… |BlobModule |react-native-github/Libraries/Blob/BlobManager.js |RCTBlobManager.mm |BlobModule.java |
| βœ… |DatePickerAndroid |react-native-github/Libraries/Components/DatePickerAndroid/DatePickerAndroid.android.js | |DatePickerDialogModule.java |
| βœ… |DevLoadingView |react-native-github/Libraries/Utilities/HMRLoadingView.ios.js |RCTDevLoadingView.m | |
| βœ… |DevSettings |N/A |RCTDevSettings.mm |DevSettingsModule.java |
| βœ… |DeviceEventManager |N/A | |DeviceEventManagerModule.java |
| βœ… |DeviceInfo |react-native-github/Libraries/Utilities/DeviceInfo.js |RCTDeviceInfo.m |DeviceInfoModule.java |
| βœ… |DialogManagerAndroid |N/A | |DialogModule.java |
| βœ… |ExceptionsManager |react-native-github/Libraries/Core/ExceptionsManager.js |RCTExceptionsManager.m |ExceptionsManagerModule.java |
| βœ… |FileReaderModule |N/A |RCTFileReaderModule.m |FileReaderModule.java |
| βœ… |HeadlessJsTaskSupport |N/A | |HeadlessJsTaskSupportModule.java |
| βœ… |I18nManager |react-native-github/Libraries/ReactNative/I18nManager.js |RCTI18nManager.m |I18nManagerModule.java |
| βœ… |ImageEditor |N/A |RCTImageEditingManager.m |ImageEditingModule.java |
| βœ… |ImageStore |N/A |RCTImageStoreManager.m | ImageStoreManager.java |
| βœ… |IntentAndroid |react-native-github/Libraries/Linking/Linking.js | |IntentModule.java |
| βœ… |JSCHeapCapture |react-native-github/Libraries/Utilities/HeapCapture.js | |JSCHeapCapture.java |
| βœ… |JSCSamplingProfiler |react-native-github/Libraries/Performance/SamplingProfiler.js | |JSCSamplingProfiler.java |
| βœ… |JSDevSupport |react-native-github/Libraries/Utilities/JSDevSupportModule.js | |JSDevSupport.java |
| βœ… |KeyboardObserver |react-native-github/Libraries/Components/Keyboard/Keyboard.js (NativeEventEmitter) |RCTKeyboardObserver.m | |
| βœ… |LinkingManager |react-native-github/Libraries/Linking/Linking.js |RCTLinkingManager.m | |
| βœ… |ModalManager |N/A |RCTModalManager.m | |
| βœ… |NativeAnimatedModule |react-native-github/Libraries/Animated/src/NativeAnimatedHelper.js |RCTNativeAnimatedModule.m |NativeAnimatedModule.java |
| βœ… |Networking |react-native-github/Libraries/Network/RCTNetworking.android.js |RCTNetworking.mm |NetworkingModule.java |
| βœ… |react-native-github/Libraries/Network/RCTNetworking.ios.js |
| βœ… |PermissionsAndroid |react-native-github/Libraries/PermissionsAndroid/PermissionsAndroid.js | |PermissionsModule.java |
| βœ… |PlatformConstants |react-native-github/Libraries/Utilities/Platform.android.js |RCTPlatform.m |AndroidInfoModule.java |
| βœ… |react-native-github/Libraries/Utilities/Platform.ios.js |
| βœ… |RedBox |N/A |RCTRedBox.m | |
| βœ… |SettingsManager |react-native-github/Libraries/Settings/Settings.ios.js |RCTSettingsManager | |
| βœ… |SourceCode |react-native-github/Libraries/Share/Share.js |RCTSourceCode.m |SourceCodeModule.java |
| βœ… |TVNavigationEventEmitter |react-native-github/Libraries/Components/AppleTV/TVEventHandler.js |RCTTVNavigationEventEmitter.m | |
| βœ… |TimePickerAndroid |react-native-github/Libraries/Components/TimePickerAndroid/TimePickerAndroid.android.js | |TimePickerDialogModule.java |
| βœ… |react-native-github/Libraries/Components/TimePickerAndroid/TimePickerAndroid.ios.js |
| βœ… |Timing |react-native-github/Libraries/Core/Timers/JSTimers.js |RCTTiming.m |Timing.java |
| βœ… |ToastAndroid |react-native-github/Libraries/Components/ToastAndroid/ToastAndroid.android.js | |ToastModule.java |
| βœ… |UIManager | |RCTUIManager.m |UIManagerModule.java |
| βœ… |WebSocketModule |react-native-github/Libraries/WebSocket/WebSocket.js |RCTWebSocketModule.m |WebSocketModule.java |

Flow Good first issue Help Wanted Native Module Locked Discussion

Most helpful comment

As of https://github.com/facebook/react-native/commit/18fededae085b53b01e54a7ed27e32c2318e7cae, all 42 PRs have now been merged! Thank you for all the contributions to this effort!

I'll close this issue for now.

All 66 comments

@RSNara any examples on how to handle event emitter classes?

any examples on how to handle event emitter classes?

export interface Spec extends TurboModule {
  +getConstants: () => {|
    // any constants it exports
  |};

  // add any exported methods

  // Then add these 2 methods:

  // Events
  +addListener: (eventName: string) => void;
  +removeListeners: (count: number) => void;
}

eric: I changed the comment for listeners to be generic

Also worth noting: there are some places in the code base that directly access constants in JS, they need to be changed to be grabbed from getConstants.

Example:

const DeviceInfo = require('./DeviceInfo');
dims = DeviceInfo.Dimensions;

becomes:

const DeviceInfo = require('./DeviceInfo');
dims = DeviceInfo.getConstants().Dimensions;

luckily, flow will complain about these if you do your typing right! So, just update the callsites.

Hey! I'd like to help with Linking module.

Hi, I would like to help with DeviceInfo

@michalchudziak, @gedeagas both were done by &ericlewis’ PRs

PermissionAndroid/TimePickerAndroid are still open I think

I'll go with PermissionsAndroid then

Sure, no problem! Is it possible to work on AccessibilityManager & AccessibilityInfo?

Edit: I'm working on it :)

I work on ToastAndroid

Taking Networking

Working on Timing

Working on WebSocketModule

~Taking ToastAndroid~ someone already taken

Working on HeapCapture

Working on IntentAndroid

Working on ModalManager

@RSNara what about the case where the native method definition differs on iOS and Android?

This is what I did sofar for the close method of WebSocketModule:

// Native close method definition on Android
declare function close(code: number, reason: string, socketID: number): void;
// Native close method definition on iOS
declare function close(socketID: number): void;

export interface Spec extends TurboModule {
  +connect: (
    url: string,
    protocols: ?Array<string>,
    options: ?{headers?: {origin?: string}},
    socketID: number,
  ) => void;
  +send: (message: string, socketID: number) => void;
  +sendBinary: (base64String: string, socketID: number) => void;
  +ping: (socketID: number) => void;
  +close: typeof close;

  // RCTEventEmitter
  +addListener: (eventName: string) => void;
  +removeListeners: (count: number) => void;
}

@jeanregisser I had similar issue with Networking module: https://github.com/facebook/react-native/pull/24892 and after consulting ideas with @rickhanlonii we decided to create two native modules for now: NativeNetworkingIOS and NativeNetworkingAndroid as even when using Platform.OS check, Flow mixed 2 definitions resulting in errors of wrong signature being used.

Working on TimePickerAndroid

Working on TVNavigationEventEmitter

Working on SourceCode

~Working on NativeAnimatedModule~ Already taken by @thymikee

Taking ExceptionsManager Taken by @thymikee .

Taking UIManager

Working on AlertManager

Taking ImageLoader

can I take DevSettings?

I'll take PlatformConstants Platform.android.js

I will take JSCSamplingProfiler

Will take FileReaderModule

i'll take DevLoadingView

I will take JSDevSupport

Taking NativeAnimatedModule

Thank you all for doing the work! We're halfway there :)

Taking HeadlessJsTaskSupport

Will take I18nManagerModule

taking BlobModule

Working on DialogManagerAndroid

taking DatePickerAndroid

Taking DeviceEventManager

Taking ImageViewManager

taking AnimationDebugModule

@uqmessias:

Actually viewmanagers shouldn’t be typed, re: ImageViewManager

@fkgozali
I was almost finishing, but I'll stop it and keep the current code in a separated branch.

RedBox remaining.

@fkgozali can you check the ImageLoader mapping ios and android files in the table: RCTImageEditingManager.m and ImageLoaderModule.java look like something wrong?

@yjose,
I was working on something related to this one

@uqmessias hmm ok

Actually` viewmanagers shouldn’t be typed, re: ImageViewManager

in this case, I think they mean ImageEditor which have the correct ios and android files RCTImageEditingManager.m and ImageEditingManager.java?
cc @RSNara @fkgozali

@yjose i think you are correct

working on ImageEditor

We got 'em all i think

Taking Platform.ios.js

I would like to help with AppState . Let's get started right away.

@mitulsavani AppState is already done and merged. Please have a look at "NativeModule List" section of the original post. Looks like the only one left is "TimePickerAndroid.ios.js" – feel free to grab it while it's free! (and please check https://github.com/facebook/react-native/pull/24897/files because it may overlap, so it may be better to cooperate with the author, @jeanregisser, on this one)

I would certainly like to claim that. Although i have no clue how and what needs to change/add, but I want to at least give a shot! @thymikee

@mitulsavani @thymikee indeed https://github.com/facebook/react-native/pull/24897 addresses TimePickerAndroid.ios.js as well.

@thymikee anything left that I work on ?

Thanks all for the PRs, we're going through them one by one to ensure nothing else breaks in FB internal infra. Stay tuned!

It looks like ImageStore got left out. For anyone interested, feel free to claim it.

I hereby claim for it.

DevSettings was also left out, feel free to claim.

I will claim the β€˜DevSetting’

As of https://github.com/facebook/react-native/commit/18fededae085b53b01e54a7ed27e32c2318e7cae, all 42 PRs have now been merged! Thank you for all the contributions to this effort!

I'll close this issue for now.

In order to get a sense of how the codegen output looks like, https://github.com/facebook/react-native/commit/b1bf133d69cf06629fb1beeeaf23ccd418bec242 checked them in, based on all the Flow types you all have added. This is not used yet, and we're working on migrating each module to use it gradually.

Hi,

In the instructions, we can read:

Create a Spec file called NativeXYZ.js, where XYZ is the name of the NativeModule.

How strict is this rule?

There's no NativePlatformConstants.js anywhere, rather there I see both NativePlatformConstantsIOS.js and NativePlatformConstantsAndroid.js. Is that going to through a wrench in the cogs?

https://github.com/react-native-community/discussions-and-proposals/issues/40#issuecomment-537056284

Was this page helpful?
0 / 5 - 0 ratings