skip to content

In React Native 0.87, how does a component subscribe to a Turbo Module event declared with CodegenTypes.EventEmitter, and how does it stop listening?

level: juniorimportance: should knowfreq 30%

answer

  1. the emitter is a function, not a name
  2. returns an EventSubscription
  3. remove() in the effect cleanup
  4. one payload argument per event
  5. no buffering before the first handler

basics

~10 s

Call the spec's emitter property as a function with a handler, for example NativeHeartRate.onHeartRate(handler); it returns an EventSubscription, and calling its remove() in the effect cleanup stops the handler when the screen unmounts.

solid answer

~30 s

A spec line such as `readonly onHeartRate: CodegenTypes.EventEmitter<HeartRateSample>` turns into a function on the module object. Inside `useEffect` you call `NativeHeartRate.onHeartRate(sample => ...)`, which returns an `EventSubscription`, and the cleanup calls `subscription.remove()`. The handler receives one typed payload per emission, delivered on the JS thread. Native code fires it through the Codegen-generated `emitOnHeartRate` method. An emission with no handler registered is simply dropped, so subscribe before you start the native stream, and stop the stream yourself when the last listener goes. The older pattern, `new NativeEventEmitter(module).addListener('eventName', handler)`, used untyped string names on a global emitter and survives only in legacy modules.

code

tsx · 21 lines
tsx
import {useEffect, useState} from 'react';
import {Text} from 'react-native';
import NativeHeartRate from './specs/NativeHeartRate';

export function HeartRateReadout() {
  const [bpm, setBpm] = useState<number | null>(null);

  useEffect(() => {
    const subscription = NativeHeartRate.onHeartRate(sample => {
      setBpm(sample.bpm);
    });
    NativeHeartRate.startStreaming();

    return () => {
      NativeHeartRate.stopStreaming();
      subscription.remove();
    };
  }, []);

  return <Text>{bpm == null ? 'Waiting for strap' : `${bpm} bpm`}</Text>;
}

go deeper

for a junior

Remember that the spec's emitter is called like a function with a handler, and that the returned subscription must be removed when the screen unmounts.

for a middle

Explain the spec declaration, the single typed payload, delivery on the JS thread, and why nothing is buffered before the first handler subscribes.

for a senior

Show how you pair subscription with explicit native start and stop calls, avoid double handlers on remount, and migrate a library off string-named NativeEventEmitter events.

for a principal

Weigh typed per-module emitters against a legacy global event bus for a shared SDK: naming collisions, payload drift between platforms, and who owns stopping expensive native streams.

## What the spec declares A **Turbo Module** is a native module described by a typed spec file. Besides methods, the spec can declare **event emitters**: read-only properties typed with `CodegenTypes.EventEmitter<T>`, where `T` is the payload type. For a cycling app that reads a Bluetooth heart-rate strap, the spec might say: ```typescript import type {TurboModule, CodegenTypes} from 'react-native'; import {TurboModuleRegistry} from 'react-native'; export type HeartRateSample = {bpm: number; timestamp: number}; export interface Spec extends TurboModule { startStreaming(): void; stopStreaming(): void; readonly onHeartRate: CodegenTypes.EventEmitter<HeartRateSample>; } export default TurboModuleRegistry.getEnforcing<Spec>('NativeHeartRate'); ``` In React Native's own type definitions an `EventEmitter<T>` is simply a function type: it takes a handler `(T) => void | Promise<void>` and returns an `EventSubscription`. Codegen rejects an emitter property declared as nullable, and it rejects a nullable payload type. ## Subscribing from a component Because the emitter is a function, subscribing means **calling the property**: 1. Inside `useEffect`, call `NativeHeartRate.onHeartRate(handler)` and keep the returned `EventSubscription`. 2. Start the native stream (here `startStreaming()`) after the handler is attached. 3. In the cleanup, stop the stream and call `subscription.remove()`. The handler runs on the **JS thread**. Native code may emit from any thread; React Native schedules each delivery onto the JavaScript runtime, so the handler can safely call `setState`. ## What the handler receives - **Exactly one argument** per emission. The C++ emitter behind the property accepts at most one payload value, so an event that needs several values uses an object type such as `HeartRateSample`. - **The spec's type, not a guarantee.** On Android, Codegen gives the native side an `emitOnHeartRate(value)` method that takes a `ReadableMap` for an object payload; on iOS the module extends the generated `NativeHeartRateSpecBase` class and calls `emitOnHeartRate:` with a dictionary. The map keys are filled in by hand, so a typo in native code produces a payload that does not match the TypeScript type. - **Only live listeners.** Each emission is sent to the handlers registered at that moment. Nothing is queued, so a reading emitted before the component subscribed is lost. ## Cleanup and what leaks Unmounting a component does **not** detach its handler. Without `remove()`, the closure keeps running after the screen is gone and keeps its captured state alive; mounting the screen again adds a second handler, so every reading is processed twice. The handler type also allows returning a Promise, so an `async` handler is accepted, but the emitter does not wait for it: the next reading can arrive while an earlier handler is still awaiting, so handlers that write to storage must tolerate overlap. There is a second, native-side leak. The generated base class adds only `emit...` methods; it does not tell native code when JavaScript listeners come and go. A module that powers a radio, a sensor or a timer therefore needs **explicit start and stop methods**, and the screen must call the stop method in the same cleanup, or the strap keeps notifying and draining battery with nobody listening. ## The legacy NativeEventEmitter pattern Before spec-level emitters (Codegen support for Java and Objective-C modules arrived in 0.76), modules emitted events by name: | | `CodegenTypes.EventEmitter` | legacy `NativeEventEmitter` | |---|---|---| | JS subscription | `Module.onHeartRate(handler)` | `new NativeEventEmitter(Module).addListener('heartRate', handler)` | | Event name | a typed spec property | a free-form string | | Payload type | checked by Codegen | untyped | | Delivery | per-module emitter | one global `RCTDeviceEventEmitter`, so names must be unique app-wide | | Native side | generated `emitOn...` method | iOS `RCTEventEmitter` subclass with `supportedEvents` and `sendEventWithName:body:`; Android `emitDeviceEvent` | | Listener counting | none | the module exposes `addListener(eventName)` and `removeListeners(count)`, which JS calls as listeners come and go | On iOS, `new NativeEventEmitter()` throws if it is given no module. You will still meet this pattern in older libraries; new modules should declare typed emitters. ## Common mistakes - Passing an event name string to the emitter property, as if it were `addListener`. - Forgetting `remove()`, which doubles handlers on every remount. - Starting the native stream before subscribing and losing the first readings. - Expecting the handler to receive several positional arguments. - Subscribing during render instead of in an effect, which adds a fresh handler on every re-render.

  • How does the native side fire onHeartRate?
    Codegen generates an `emitOnHeartRate` method. On Android the generated abstract spec class exposes a protected `emitOnHeartRate(value)` that takes a `ReadableMap` for an object payload, built with `Arguments.createMap()`. On iOS the module extends the generated `NativeHeartRateSpecBase` and calls `[self emitOnHeartRate:@{...}]`. In both cases the keys must match the TypeScript type by hand.
  • Why does the cleanup also call stopStreaming()?
    The generated base class only adds emit methods; it has no hook that tells native code how many JavaScript listeners exist. If the screen removes its subscription but never stops the stream, the native Bluetooth notifications keep firing, emissions are dropped, and the strap keeps using radio and battery for nothing.

The emitter is like tuning a radio to a live station: you hear only what is broadcast while you are tuned in, nothing is recorded for later, and turning your radio off does not stop the transmitter unless you phone the station.

saying these in an interview costs you the question

  • Calling the emitter property returns a Promise of the next event
  • Events emitted before subscribing are queued and replayed later
  • Unmounting the component removes its native event handler automatically
  • A typed event is subscribed by passing its name string to the emitter
  • Native code can pass several positional arguments to one event handler
  • NativeEventEmitter with string names is the recommended way for new modules