Data Flow
This document traces the complete data flow for location tracking and geofencing -- from the React hook layer through the TurboModule bridge, into native services, and back to JavaScript via event emitters.
High-Level Flow
Start Tracking Flow
When startTracking() is called, data crosses three boundaries before reaching the location provider.
Step 1: TypeScript API (src/index.tsx)
The toTrackingOptionsSpec() utility in src/utils/trackingOptionsMapper.ts handles two conversions that Codegen cannot:
- Enum-to-string --
LocationAccuracyandNotificationPriorityenum values become plain strings. - Object-to-JSON --
notificationActions(capped at 3 items) andnotificationOptionsare serialized to JSON strings.
Step 2: TurboModule Bridge
The TrackingOptionsSpec interface crosses the bridge with all values in Codegen-compatible types (strings, numbers, booleans). The native side receives a ReadableMap (Android) or NSDictionary (iOS) and reconstructs the typed options.
Step 3: Native Service Initialization
- Android:
BackgroundLocationModuleparses theReadableMapinto a KotlinTrackingOptionsdata class, saves tracking state to Room DB, and startsLocationServiceas a foreground service. - iOS:
BackgroundLocation.mmforwards toLocationManagerWrapper.swift, which configuresCLLocationManagerwith the mapped accuracy, distance filter, and background mode settings.
Location Update Flow
Once tracking is active, location updates flow from the OS through the native layer and back to JavaScript.
Android Event Pipeline
Android uses SharedFlow singletons to decouple event producers from consumers:
| SharedFlow | Sealed Interface | Producer | Consumer |
|---|---|---|---|
LocationEventFlow | LocationEvent (Update, Error, Warning) | LocationEventEmitter | BackgroundLocationModule |
GeofenceEventFlow | GeofenceEvent (Transition) | GeofenceEventEmitter | BackgroundLocationModule |
NotificationActionFlow | NotificationActionEvent (ActionClicked) | NotificationActionReceiver | BackgroundLocationModule |
All three flows share the same configuration: replay = 0, extraBufferCapacity = 64, onBufferOverflow = DROP_OLDEST. The non-suspending tryEmit() API ensures producers never block.
BackgroundLocationModule collects from all three flows using coroutine Jobs scoped to moduleScope (SupervisorJob + Dispatchers.Main). Each collected event is forwarded to JavaScript via RCTDeviceEventEmitter.
iOS Event Pipeline
iOS uses RCTEventEmitter directly. LocationManagerDelegate processes each CLLocation from the didUpdateLocations: callback, writes to Core Data, and emits to JavaScript via sendEvent(withName:body:). There is no intermediate flow layer -- the delegate bridges directly to the React Native event system.
Platform Comparison
| Aspect | Android | iOS |
|---|---|---|
| Event decoupling | SharedFlow singletons (3 flows) | Direct delegate-to-emitter |
| Buffer strategy | 64-element ring buffer, DROP_OLDEST | No buffer (immediate emit) |
| Thread model | Coroutine collection on Main dispatcher | Main thread delegate callbacks |
| Producer API | Non-suspending tryEmit() | Synchronous sendEvent() |
| Consumer lifecycle | Coroutine Jobs tied to moduleScope | RCTEventEmitter managed by React Native |
Event Payload Format
Both platforms emit identical event payloads to JavaScript:
onLocationUpdate
{
tripId: string;
latitude: number;
longitude: number;
timestamp: number;
accuracy?: number;
altitude?: number;
speed?: number;
bearing?: number;
verticalAccuracy?: number;
speedAccuracy?: number;
bearingAccuracy?: number;
elapsedRealtimeNanos?: number;
provider?: string;
isMocked?: boolean;
}
onLocationError
{
tripId: string;
type: 'PERMISSION_REVOKED' | 'PROVIDER_ERROR';
message: string;
}
onLocationWarning
{
tripId: string;
type: 'SERVICE_TIMEOUT' | 'TASK_REMOVED' | 'LOCATION_UNAVAILABLE';
message: string;
}
onNotificationAction
{
tripId: string;
actionId: string;
}
Storage Flow
Write Path
Both platforms use the same batching strategy:
- Locations are buffered in memory.
- Flush triggers when the buffer reaches 10 items or a 5-second timer fires.
- On flush failure, items are re-added to the buffer for retry.
Read Path
Every read operation calls forceFlush() first to drain pending writes, guaranteeing that the returned data includes the most recent buffered locations.
Geofencing Data Flow
Registration
Geofence regions go through three transformations before reaching the platform:
- Validation --
validateGeofenceRegion()checks identifier, radius, coordinates, and metadata. - Duplicate check -- Queries active geofences to prevent duplicate identifiers.
- Serialization --
serializeGeofenceRegion()converts the typedGeofenceRegionto a JSON string for the bridge.
Transition Events
Transition events are both persisted (for later retrieval via getGeofenceTransitions()) and emitted in real-time to JavaScript.
Stop Tracking Flow
The stop token is the critical mechanism that prevents the recovery system from restarting a service that was intentionally stopped. It is set synchronously (commit() on Android, synchronous UserDefaults write on iOS) during stopTracking() and checked at three points in the recovery pipeline.
Recovery Flow
Android (WorkManager)
iOS (Significant Location Monitoring)
Hook Data Flow
useLocationUpdates
The hook uses three data sources:
- Mount hydration -- One-time
getLocations()call on mount to load persisted data. - Live events --
NativeEventEmittersubscription for real-time updates. - Foreground re-hydration --
AppStatelistener triggersgetLocations()when the app returns from background, catching any updates that arrived while the JS bridge was inactive.
Next Steps
- Android Native Architecture -- Deep dive into SharedFlow, coroutines, and the foreground service.
- iOS Native Architecture -- Deep dive into CLLocationManager, Core Data, and the permission escalation flow.