@santoshpk/react-native-background-location-tracking
v1.0.0
Published
Continuous background & foreground location tracking for React Native (new arch / TurboModule)
Maintainers
Readme
react-native-background-location-tracking
Continuous foreground and background location tracking for React Native, built on the new architecture (TurboModule). A free, open-source alternative to the paid react-native-background-geolocation, with honest docs about the OS limits.
const sub = onLocation((loc) => console.log(loc.latitude, loc.longitude));
await requestPermissions();
await startTracking({ interval: 5000 });Capability matrix
Honesty first — this is what v1 actually does:
| Scenario | Android | iOS | Status | |----------|---------|-----|--------| | App foreground | ✅ continuous | ✅ continuous | v1 | | App backgrounded (process alive) | ✅ foreground service | ✅ background location mode | v1 | | App killed / swiped away | ⚠️ stops | ⚠️ stops | out of scope — future | | Device reboot | ⚠️ stops | ⚠️ stops | out of scope — future |
In scope v1: continuous tracking while the app process is alive — foreground and backgrounded, both platforms. Kill-state / reboot recovery (Android START_STICKY/BOOT_COMPLETED, iOS significant-location-change / geofencing) is not shipped and left to the user.
Requirements
| | Minimum |
|---|---|
| React Native | 0.76+ with the new architecture enabled |
| Android | minSdkVersion 24, Google Play services on the device (Fused Location Provider) |
| iOS | inherits your RN version's min_ios_version_supported — the podspec pins nothing of its own |
New architecture only. This library is a TurboModule. There is no old-architecture / bridge fallback, and none is planned — if you're on the legacy architecture, upgrade first.
Installation
npm install @santoshpk/react-native-background-location-tracking
# or
yarn add @santoshpk/react-native-background-location-trackingThen, for iOS:
cd ios && pod installRebuild the app after installing — this ships native code, so a Metro reload is not enough:
npx react-native run-android
npx react-native run-iosExpo
Works with Expo, but not in Expo Go — Expo Go can't load custom native modules. Use a development build:
npx expo prebuild
npx expo run:android
npx expo run:iosAdd the iOS Info.plist keys below via app.json → ios.infoPlist.
Android setup
Nothing to edit. Permissions and the <service> entry are merged from the library manifest automatically:
ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION,
FOREGROUND_SERVICE, FOREGROUND_SERVICE_LOCATION, POST_NOTIFICATIONS
<service android:foregroundServiceType="location" />At runtime, requestPermissions() walks the prompts in the order the OS demands:
ACCESS_FINE_LOCATIONPOST_NOTIFICATIONS(Android 13+)ACCESS_BACKGROUND_LOCATION— user must pick "Allow all the time" (Android 10+)
Non-negotiable OS facts:
- Android 14+ requires the
locationforeground-service type (already declared here). - The persistent notification cannot be hidden — the OS requires it.
iOS setup
Add to Info.plist — all three keys:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Why you need location while the app is open.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Why you need location in the background.</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>Then enable Signing & Capabilities → Background Modes → Location updates in Xcode (this is what writes UIBackgroundModes if you prefer the GUI).
Background tracking needs the user to grant "Always" authorization. The module sets allowsBackgroundLocationUpdates = true and pausesLocationUpdatesAutomatically = false. Without the location background mode the library emits an onError warning and keeps running foreground-only.
This library never fakes background execution with silent-audio/VoIP tricks — those get you rejected from the App Store.
Quick start
A complete, working component — copy it as-is:
import { useEffect, useState } from 'react';
import { Button, Text, View } from 'react-native';
import {
requestPermissions,
startTracking,
stopTracking,
onLocation,
onError,
type LocationData,
} from '@santoshpk/react-native-background-location-tracking';
export default function Tracker() {
const [last, setLast] = useState<LocationData | null>(null);
const [running, setRunning] = useState(false);
// Subscribe BEFORE starting, so you don't miss the first fixes.
useEffect(() => {
const sub = onLocation(setLast);
const errSub = onError((e) => console.warn(e.message));
return () => {
sub.remove(); // unmount = unsubscribe, else it leaks and keeps firing
errSub.remove();
};
}, []);
const start = async () => {
const granted = await requestPermissions({
title: 'Location needed',
message: 'MyApp records your route.',
buttonPositive: 'OK',
});
if (!granted) return;
await startTracking({
interval: 5000,
accuracy: 'high',
notificationTitle: 'Tracking active',
notificationText: 'Recording your location',
});
setRunning(true);
};
const stop = () => {
stopTracking(); // stops native GPS, the Android service + notification
setRunning(false);
};
return (
<View>
<Text>{last ? `${last.latitude}, ${last.longitude}` : 'no fix yet'}</Text>
<Button title="Start" onPress={start} disabled={running} />
<Button title="Stop" onPress={stop} disabled={!running} />
</View>
);
}Cleanup is two independent levels — do both when you're done:
| Call | What it stops |
|------|---------------|
| sub.remove() | Your JS callback. Native keeps tracking. |
| stopTracking() | Native GPS, foreground service, notification. |
Running the example app
The repo ships a working example that exercises every API.
git clone https://github.com/santoshP0/react-native-background-location-tracking.git
cd react-native-background-location-tracking
yarn install
yarn example start # Metro, leave it running
yarn example android # in a second terminal
yarn example iosThen tap Start. Coordinates stream into the list; the interval is editable in the UI.
Faking movement on an emulator/simulator
A stationary emulator has no GPS — feed it positions:
# Android (emulator must be a Google APIs image — Fused needs Play services)
adb emu geo fix 77.5946 12.9716 # note: LONGITUDE first
adb emu geo fix 77.6100 12.9900 # move it
# iOS
xcrun simctl location booted set 12.9716,77.5946 # note: LATITUDE firstFixes from an emulator arrive with fromMockProvider: true, and speed/course stay 0 unless you use the simulator's route-playback feature.
Verifying background tracking really works
# Android — background the app, then confirm the service is still alive
adb shell input keyevent KEYCODE_HOME
adb shell dumpsys activity services <your.package> | grep isForeground
# expect: isForeground=true ... types=0x00000008 (= FOREGROUND_SERVICE_TYPE_LOCATION)
# watch events keep arriving while backgrounded
adb logcat -v time ReactNativeJS:V '*:S'On iOS, background the app and watch for the blue status-bar location indicator — events keep flowing to your onLocation callback.
API
| Function | Description |
|----------|-------------|
| requestPermissions(rationale?) | Request location (+ notification + background on Android). Optional rationale { title, message, buttonPositive } explains why — shown by Android when the user previously declined. Resolves true when foreground location is granted (precise or approximate). iOS no-op — the prompt fires on startTracking, reason text comes from Info.plist. |
| startTracking(options?) | Start continuous tracking. Returns Promise<void>. |
| stopTracking() | Stop tracking and the foreground service. |
| getCurrentLocation() | One-shot fix. Returns Promise<LocationData>; always resolves or rejects, never hangs. |
| isTracking() | Returns Promise<boolean>. |
| onLocation(cb) | Subscribe to updates. Returns a subscription — call .remove(). |
| onError(cb) | Subscribe to warnings/errors. Returns a subscription. |
startTracking options
| Option | Type | Default | Notes |
|--------|------|---------|-------|
| interval | number | 5000 | ms. Android: native request rate. iOS: heartbeat (see below). |
| fastestInterval | number | = interval | ms. Android only — fastest rate your app can handle. |
| distanceFilter | number | 0 | Metres of movement between updates. |
| accuracy | 'high' \| 'balanced' \| 'low' \| 'passive' | 'high' | Accuracy/battery tradeoff. |
| notificationTitle | string | 'Location tracking active' | Android notification title. |
| notificationText | string | 'Tracking your location' | Android notification body. |
| notificationChannelName | string | 'Location tracking' | Android channel display name. |
LocationData: { latitude, longitude, accuracy, altitude, speed, course, timestamp, fromMockProvider } — timestamp is epoch millis.
How events flow
Native side emits, JS side listens — one-way:
startTracking(options)
Android: FusedLocationProviderClient fires every `interval` ms
iOS: CLLocationManager fires on movement; a heartbeat re-emits the
latest fix if nothing arrived within `interval` ms
↓
native emits the 'location' event
↓
onLocation(callback) fires with LocationDataPlatform nuance: Android's interval is the native request rate. iOS CoreLocation is movement-driven, so this library adds a heartbeat — when the device is still, the latest known position is re-emitted every interval ms with a fresh timestamp (the emission time), matching how Android's Fused provider yields a new fix every interval even while stationary. Real movement updates carry the OS's own fix time. Use distanceFilter to thin out movement updates.
While stationary the coordinates repeat — that's expected. Log the arrival time (
new Date()), notloc.timestamp, to see the interval ticking.
Rejection codes
| Code | When |
|------|------|
| PERMISSION_DENIED | Android: startTracking/getCurrentLocation without location permission. |
| NOT_AUTHORIZED | iOS: startTracking while location access is denied/restricted in Settings. |
| NO_LOCATION | Android: no fix available for getCurrentLocation. |
| LOCATION_ERROR | One-shot fix failed (both platforms). |
| MODULE_INVALIDATED | iOS: pending getCurrentLocation when React reloads/shuts down. |
| START_FAILED | Android: service failed to start. |
iOS extras you get without any setup: the OS blue location indicator while tracking in background, and on iOS 17+ a CLBackgroundActivitySession that keeps tracking alive while stationary in the background.
Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| TurboModuleRegistry.getEnforcing(...): 'BackgroundLocationTracking' could not be found | Native code not in the binary. | Rebuild (run-android / run-ios); a Metro reload won't do it. On iOS re-run pod install. |
| PlatformConstants could not be found | The app is loading a different project's Metro on port 8081. | Stop the other dev server, or run yours on another port (--port 8082 + adb reverse tcp:8081 tcp:8082). |
| No events at all on Android | Permission not granted, or emulator has no Play services. | Check requestPermissions() resolved true; use a Google APIs emulator image. |
| Events stop when the app is backgrounded (iOS) | UIBackgroundModes: location missing. | Add the key — the library emits an onError warning saying exactly this. |
| Events stop when the app is backgrounded (Android) | Background location not granted. | The user must pick "Allow all the time", not "While using the app". |
| Same coordinates repeating | Device is stationary — the fix is genuinely unchanged. | Expected. Compare arrival times, not loc.timestamp. |
| Tracking dies when the app is swiped away | By design in v1. | See the capability matrix. |
Security & privacy
What this package does — and deliberately doesn't do — with your users' location:
- No network. No storage. Location data goes from the OS straight to your JS callback. Nothing is uploaded, cached, or written to disk by this library.
- No location logging. Coordinates never appear in logcat / console output, so they can't leak through device logs.
- Service is not exported. No other app can start, stop, or feed options into the tracking service.
- Permission-guarded.
startTracking()rejects withPERMISSION_DENIEDif location permission is missing (also prevents the Android 14 foreground-service crash). - Spoof detection surface. Every fix carries
fromMockProviderso you can flag faked locations yourself. - Input clamping. Malformed options (negative intervals etc.) are clamped natively instead of crashing the service.
- No zombie tracking. If the React instance goes away, the Android service is stopped and the context reference released — no GPS burning with nobody listening, no leaked contexts.
Contributing
License
MIT
Made with create-react-native-library
