Skip to main content

Callbacks ((...) => T)

Callbacks are functions created in one language and passed to another to provide a way to "call back" later.

Nitro has a clever reference counting system to allow users to use callbacks/functions from JS safely, and without any limitations. Each callback holds a strong reference on the native side and can be called as often as needed. Once the callback is no longer used, it will be safely deleted from memory.

import { type HybridObject, iOS } from 'nitro-core'

@iOS(...)
export declare class Server extends HybridObject {
start(onNewUserJoined: (user: User) => void): void
}

Events​

Since callbacks can be safely kept in memory for longer and called multiple times, Nitro does not have a special type for an "event". It is simply a function you store in memory and call later, just like in a normal JS class. ✨

type Orientation = "portrait" | "landscape"
@iOS(...)
export declare class DeviceInfo extends HybridObject {
listenToOrientation(onChanged: (o: Orientation) => void): void
}
export * from '#nitro/DeviceInfo'

const deviceInfo = // ...
deviceInfo.listenToOrientation((o) => {
console.log(`Orientation changed to ${o}!`)
})

Callbacks that return a value ((...) => T)​

A Callback can also return a value:

Math.nitro.ts
@iOS(...)
export declare class Math extends HybridObject {
calculate(getValue: () => number): void
}
export * from '#nitro/Math'

Since JS callbacks could theoretically be called from any native Thread, callbacks are called asynchronously by default:

HybridMath.swift
class HybridMath: HybridMathSpec {
func calculate(getValue: any Callback<(), Double>) {
Task {
let result = try await getValue() // async Double
}
}
}

Async by default, sync when needed​

Callback has three calling conventions:

func call(callback: any Callback<(), Void>) {
callback.callAsync() // Promise<Void>
callback.callSync() // Void (blocking)
callback.callAndForget() // Void
}

Additionally, the call operator overload either chooses callAndForget() when the callback returns Void, or callAsync() when it returns a value.

func callVoid(callback: any Callback<(), Void>) {
callback() // Void (just like callAndForget())
}
func callResult(callback: any Callback<(), Double>) {
callback() // Promise<Double> (just like callAsync())
}
warning

Use callSync() only when you can ensure that you call it on the JS Runtime's Thread - otherwise it will throw.

Inline Callbacks​

If your callback never escapes the calling function, use Inline<...> as a performance optimization:

Math.nitro.ts
@iOS(...)
export declare class Math extends HybridObject {
calculate(getValue: Inline<() => number>): void
}
export * from '#nitro/Math'

InlineCallback<...> allows you to call back to JS inline, or escape to a Callback<...> just like before:

HybridMath.swift
class HybridMath: HybridMathSpec {
func calculate(getValue: borrowing some InlineCallback<(), Double> & ~Copyable & ~Escapable) {
let result = getValue() // Double
let callbackForLater = getValue.escape() // any Callback<(), Double>
}
}

How was it before Nitro?​

Conventionally (in legacy React Native Native Modules), a native method could only have a maximum of two callbacks, one "success" and one "failure" callback. Once one of these callbacks is called, both will be destroyed and can no longer be called later. This is why React Native introduced "Events" as a way to call into JS more than just once. This also meant that an asynchronous function could not have any callbacks, since a Promise's resolve and reject functions are already two callbacks. For example, this was not possible:

interface Camera {
startRecording(onStatusUpdate: () => void,
onRecordingFailed: () => void,
onRecordingFinished: () => void): Promise<void>
}

Thanks to Nitro's clever reference system, functions can be safely held in memory and called as many times as you like, just like in a normal JS class. This makes "Events" obsolete, and allows using as many callbacks per native method as required.