til / Returning values from Native Modules
While creating modules I found that the RCT_EXTERN_METHOD
macro doesn’t handle returning values from the Swift function it binds to (it’s the same with @ReactMethod
in Kotlin.)
They only support functions returning void
. However, there are no errors in the IDE or when building. The returned value just becomes undefined
in TypeScript when you call the function. I found it hard to debug why it didn’t work.
Here’s what we can do:
constantsToExport
#
// DeviceInfoModule.m
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)
// No exports here as we use constants
// I tried with RCT_EXTERN_METHOD(getBrand) which
// compiled, but it only returned undefined
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
@end
When overriding constantsToExport
the documentation states that we should implement requiresMainQueueSetup
. If we don’t require access to UIKit
, then we should respond with NO
.
// DeviceInfoModule.swift
@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
// Even though this returns a string, it's not
// passed through the macro.
func getBrand() -> String {
return "Apple"
}
@objc
func constantsToExport() -> [String: Any]! {
// This will become an object in JavaScript/TypeScript
return ["brand": getBrand()]
}
}
// deviceInfo.ts
import { NativeModules } from 'react-native'
const { DeviceInfoModule } = NativeModules
interface DeviceInfoInterface {
getBrand(): string
}
const deviceInfo = DeviceInfoModule.getConstants()
// Convenience methods, we could easily just export
// the constants directly if we wanted to.
export const DeviceInfo: DeviceInfoInterface = {
getBrand: () => deviceInfo.brand,
}
Promise #
To handle promises we add some arguments to our method which makes it a tiny bit more complex, but the interface becomes simpler in TypeScript. Function coloring might screw that up though.
// DeviceInfoModule.m
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)
// Define the method and set two arguments, resolve and reject
// If we want to send arguments, the resolve and reject
// need to be the last two parameters to work.
RCT_EXTERN_METHOD(getBrand:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@end
// DeviceInfoModule.swift
@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
func getBrand(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) {
resolve("Apple")
}
}
Note the leading _
because the resolve parameter is not named in the RCT_EXTERN_METHOD
above. This is how the macro works for all first arguments. I think that a named first parameter, for example, getBrand:resolver:(RCTPromiseResolveBlock)resolve
would break it.
// deviceInfo.ts
import { NativeModules } from 'react-native'
const { DeviceInfoModule } = NativeModules
interface DeviceInfoInterface {
getBrand(): Promise<string>
}
export default DeviceInfoModule as DeviceInfoInterface
Callback #
// DeviceInfoModule.m
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(DeviceInfoModule, NSObject)
// Define the method and set the callback
// Like with a promise, the callback needs to be the last argument
RCT_EXTERN_METHOD(getBrand:(RCTResponseSenderBlock)callback)
@end
// DeviceInfoModule.swift
@objc(DeviceInfoModule)
class DeviceInfoModule: NSObject {
func getBrand(_ callback: RCTResponseSenderBlock) {
// To return errors, we can follow Node's standard
// of error first, data second. Or, we can split it into
// two callbacks, one for success and one for error.
callback([NSNull(), "Apple"])
}
}
Note: We’re not allowed to use nil
instead of NSNull()
here.
// deviceInfo.ts
import { NativeModules } from 'react-native'
const { DeviceInfoModule } = NativeModules
interface DeviceInfoInterface {
getBrand(callback: (err: Error, value: string) => void): void
}
export default DeviceInfoModule as DeviceInfoInterface