In a React Native Fabric component spec, how do DirectEventHandler and BubblingEventHandler differ, and which suits a signature pad's onStrokeEnd?
answer
- one target versus a propagation path
- bubbled and captured registration names
- onXCapture exists only for bubbling
- payload arrives as event.nativeEvent
- Android exports direct or bubbling constants
basics
~20 sA direct event is delivered only to the component that emitted it. A bubbling event goes through capture and bubble phases, so ancestors registered for it, including onXCapture handlers, can see it too. A signature pad's onStrokeEnd concerns only the pad, so DirectEventHandler fits.
solid answer
~40 sBoth types come from `CodegenTypes` and describe a prop whose handler receives a `NativeSyntheticEvent<T>`, so JavaScript reads the payload from `event.nativeEvent`. The difference is in the view config Codegen generates. A `BubblingEventHandler` registers **phased** names — `bubbled: 'onStrokeEnd'` and `captured: 'onStrokeEndCapture'` — so the event is dispatched through capture and bubble phases along the ancestor path. A `DirectEventHandler` registers a single `registrationName` and reaches only the emitting component. On Android, the view manager declares the event in `getExportedCustomDirectEventTypeConstants` or `getExportedCustomBubblingEventTypeConstants` to match. An `onStrokeEnd` with `{pointCount}` means nothing to ancestors, so the proof-of-delivery pad declares it `DirectEventHandler`, and keeps bubbling for touch-like events a parent might intercept.
code
tsx · 22 linesimport {useState} from 'react';
import {View} from 'react-native';
import SignaturePad from './specs/SignaturePadNativeComponent';
export function ProofOfDelivery() {
const [hasSignature, setHasSignature] = useState(false);
return (
<View style={{flex: 1}}>
<SignaturePad
style={{height: 240}}
onStrokeEnd={event => {
// payload lives on nativeEvent
if (event.nativeEvent.pointCount > 0) {
setHasSignature(true);
}
}}
/>
{/* Confirm button enabled when hasSignature is true */}
</View>
);
}go deeper
Remember the two event types in CodegenTypes, that handlers read event.nativeEvent, and that direct means only the emitting component hears it.
Explain the generated view-config difference, registrationName versus phased bubbled and captured names, and the matching Android export methods.
Choose event routing deliberately, keep payloads small and typed, and debug platform-only events by checking the spec type against the Android export.
Define event conventions for a component library, such as direct by default and outcome enums in payloads, so components stay predictable across teams.
## Events on a Fabric native component A **Fabric native component** reports what happens in its native view — a stroke finished, a page loaded, a value changed — through **event props** declared in its spec. The Codegen parser only accepts two types for them, both exported from the root `react-native` entry point as members of the `CodegenTypes` namespace: - `CodegenTypes.DirectEventHandler<Payload>` - `CodegenTypes.BubblingEventHandler<Payload>` A plain function type is rejected with a message asking for one of the two. Both have the same JavaScript signature: the handler receives a `NativeSyntheticEvent<Payload>`, and the data is in `event.nativeEvent`. What differs is **how React Native routes the event**, which Codegen encodes in the component's generated view config. ## What Codegen generates for each | | `DirectEventHandler` | `BubblingEventHandler` | |---|---|---| | View config section | `directEventTypes` | `bubblingEventTypes` | | Registration | `registrationName: 'onStrokeEnd'` | `phasedRegistrationNames: {bubbled: 'onStrokeEnd', captured: 'onStrokeEndCapture'}` | | Who is notified | The component that emitted it | The capture and bubble phases along the ancestor path | | Capture-phase handler | None | `onStrokeEndCapture` | | Android export method | `getExportedCustomDirectEventTypeConstants` | `getExportedCustomBubblingEventTypeConstants` | | Typical use | Component-specific notifications | Touch-like interactions ancestors may observe | A **bubbling** event behaves like the press and touch events React Native developers already know: it is dispatched through a **capture** phase from the root down and a **bubble** phase from the target up, so an ancestor with a matching handler can react. A **direct** event has no phases: exactly one handler, on the emitting element, is called. ## Choosing for a signature pad A parcel-delivery app's pad might emit three events: 1. `onStrokeEnd` with `{pointCount}` — tells the screen to enable the Confirm button. Only the screen rendering the pad cares: **direct**. 2. `onSignatureExported` with `{uri}` — the result of an export command. One consumer: **direct**. 3. An event that deliberately mirrors a touch interaction, where an ancestor may legitimately want to observe or intercept it — the reason React Native's own press-style events bubble. That is the rare case where propagation is the point: **bubbling**. The rule of thumb: use **direct** unless there is a concrete reason for ancestors to hear the event. It is the simpler contract, and there is no capture variant to document. ## Emitting the event natively - **iOS:** the component view casts its `_eventEmitter` to the generated `SignaturePadEventEmitter` and calls `onStrokeEnd(SignaturePadEventEmitter::OnStrokeEnd{...})` with a generated payload struct. - **Android:** the view dispatches an `Event` subclass whose `getEventName()` matches the prop, through the event dispatcher it gets from `UIManagerHelper`, and the view manager exports the event under the matching direct or bubbling constants — React Native's own guide shows `getExportedCustomBubblingEventTypeConstants` for its bubbling example. Keep the two descriptions consistent: the spec's handler type and the Android export describe the same event, and React Native's guide treats that export as part of wiring any component event on Android. When an event works on iOS but not on Android, the export method and the `getEventName()` string are the first places to look. ## Payload design - Keep payloads **small and typed**: `CodegenTypes.Int32`, `Double`, `string`, `boolean`, nested objects of those. - Send **results, not streams**: a stroke-end summary rather than every touch point, so JavaScript is not flooded. - Use a **string union** for outcome fields (`result: 'success' | 'error'`), as React Native's WebView example does. ## How names flow from the spec - The **prop name** (`onStrokeEnd`) is the registration name JavaScript handlers attach to. - On **iOS**, Codegen derives the emitter method and payload struct from it: `SignaturePadEventEmitter::OnStrokeEnd`, emitted with `onStrokeEnd(...)`, following the pattern of React Native's WebView example (`CustomWebViewEventEmitter::OnScriptLoaded`). - On **Android**, the hand-written `Event` subclass returns the same string from `getEventName()`, and the manager exports it under that key. This is the one place the name is typed by hand, so it is where typos hide. ## Mistakes interviewers listen for - Believing direct events "skip the JavaScript thread" or are faster in a way that matters — the difference is routing, not transport. - Declaring event props as plain function types. - Reading the payload from `event` instead of `event.nativeEvent`. - Choosing bubbling by default and then documenting an `onXCapture` prop nobody needs.
- What names does Codegen register for a bubbling onStrokeEnd event?It generates `phasedRegistrationNames` with `bubbled: 'onStrokeEnd'` and `captured: 'onStrokeEndCapture'` under the component's `bubblingEventTypes`. That is why a bubbling event comes with a capture-phase prop, while a direct event gets only `registrationName: 'onStrokeEnd'` under `directEventTypes`.
- An event fires on iOS but never on Android. What do you check first?That the Android view manager exports the event under the right method: `getExportedCustomDirectEventTypeConstants` for a `DirectEventHandler` or `getExportedCustomBubblingEventTypeConstants` for a `BubblingEventHandler`. Then check that the Android `Event`'s `getEventName()` matches the prop name, and that it is dispatched with the view's id and surface.
saying these in an interview costs you the question
- Direct events bypass the JavaScript thread, so they are always faster
- Any function type works for an event prop; the Codegen types are optional
- The event payload is the handler's argument itself, not event.nativeEvent
- A direct event also triggers an onXCapture handler on ancestors
- Bubbling should be the default choice for every component event