react-native-live-tracking-map
v1.0.0
Published
Drop-in live tracking map for React Native — customer side (watch a moving partner) and driver side (device GPS, compass heading, follow camera, throttled location reporting, arrival + off-route detection) in one component. Includes automatic Android Goog
Maintainers
Readme
react-native-live-tracking-map
A single, drop-in React Native component for live location tracking screens
(delivery, cab, courier, field service, fleet — anything with a moving point
and a destination). Built on top of react-native-maps.
Covers both sides of the same job:
- Customer side — watch someone else move. You feed coordinates in from your socket/poll; the marker glides, the route draws, the ETA updates.
- Driver side (
mode="driver") — this device is the moving point. The package reads the GPS and compass, throttles the position reports back to your API, follows the camera navigation-style, detects arrival and going off-route, and hands off to Google/Apple Maps for voice navigation.
Features
- Marker glides smoothly between location updates instead of snapping
- Marker rotates to face its direction of travel (bearing)
- Camera auto-fit / smooth
animateCameraeasing between the two points - Real, road-following route via a pluggable routing provider: Google Directions, Mapbox Directions, OSRM (free, no key), or your own backend
- Native live traffic layer, plus traffic-aware ETA from Google/Mapbox
- Optional built-in ETA/distance badge
- Fully offline-safe default: with no routing provider configured, it draws a curved line with zero network calls
- Imperative controls via ref:
fitToMarkers,animateToPartner,animateToUser,refreshRoute
Driver side (mode="driver"):
- Follow camera —
course-up(map rotates to travel direction, tilted) orcenter, pausing when the driver pans the map and auto-resuming - Compass heading — marker faces the real heading even while stopped, with shortest-path rotation (no spinning the long way round through north)
useDriverTrackinghook — fast first fix, high-accuracy watcher, accuracy filtering, and API reports throttled to "moved 20 m OR 15 s passed"- Arrival detection —
onArrivefires once insidearrivalRadiusMeters, and re-arms when the destination changes (pickup → drop) - Off-route detection — measured against the drawn route, triggers an
automatic reroute and
onOffRoute - Waypoints — thread the route through intermediate stops (multi-drop)
- Turn-by-turn handoff —
openExternalNavigation()launches Google Maps / Apple Maps with the destination (and stops) preloaded
This does not include turn-by-turn voice navigation — that requires a provider-locked native navigation SDK. This covers live position tracking, real route, and traffic-aware ETA.
Install
npm install react-native-live-tracking-map
# or
yarn add react-native-live-tracking-mapreact-native-maps is a peer dependency — React Native's CLI autolinking
only scans the direct dependencies in your app's own package.json, so a
native module has to live there to be discovered, no matter how any wrapper
package declares it. This package's postinstall script handles that part
for you automatically: it adds react-native-maps straight into your app's
package.json and installs it, so you don't have to run that step by hand.
What can't be automated (true for any native React Native module, not specific to this package) is the native link + rebuild:
cd ios && pod install && cd ..
npx react-native run-ios # or run-androidA JS reload / Fast Refresh will never pick up a native module — it requires an actual rebuild of the native app.
Driver side — optional extra modules
The driver features read the device's own sensors, so they need a geolocation module (and, optionally, a compass). These are optional peer dependencies, loaded lazily — a customer-only app never has to install them:
yarn add @react-native-community/geolocation react-native-compass-heading
# react-native-geolocation-service works too; it's picked up automaticallyIf neither geolocation module is present, useDriverTracking reports a
no-module error instead of throwing, and the map still renders whatever
coordinates you pass it by hand.
Make sure react-native-maps itself is configured for your platform. iOS's
default map (PROVIDER_DEFAULT, Apple Maps) needs no API key at all — the
Android side is what usually means editing AndroidManifest.xml and
strings.xml by hand. This package automates that part; see below.
Android Google Maps API key — no manual XML editing
Why this can't just be a component prop: Android reads its Maps key from
AndroidManifest.xml at native app startup, before any JS runs. A prop on
<LiveTrackingMap apiKey="..."> physically can't reach that — that prop only
feeds the Directions/Mapbox HTTP calls (JS, so it can be a prop, and already
is). What this package gives you instead is the closest thing to "pass it in
and go": one key, no hand-editing either XML file.
Pick whichever fits your workflow — all three end up writing the same two
files (android/app/src/main/res/values/strings.xml and
AndroidManifest.xml), and running any of them again (e.g. to rotate the key)
updates the existing entries rather than duplicating them:
# a) One-off / CI: run it whenever you like
npx react-native-live-tracking-map-setup --key=YOUR_GOOGLE_MAPS_KEY// b) Zero manual steps at all — add this to your APP's package.json,
// then `yarn install` / `npm install` does it automatically:
{
"reactNativeLiveTrackingMap": {
"androidApiKey": "YOUR_GOOGLE_MAPS_KEY"
}
}# c) Or export an env var before installing — also picked up automatically:
export GOOGLE_MAPS_API_KEY=YOUR_GOOGLE_MAPS_KEY
yarn add react-native-live-tracking-mapIf none of these are set, postinstall just prints the one command instead
of two pages of manual XML steps. Either way you still need a real rebuild
afterwards (npx react-native run-android) — a JS reload never picks up a
manifest change, same as any other native config.
Keep the key restricted (package name + SHA-1 fingerprint) on the Google Cloud Console side — same as any Android app shipping a client key, secret or not; see the Security note below.
API keys — routing (Directions/Mapbox), passed as a prop
For routingProvider="google" or "mapbox", don't hardcode the key in your
component tree. Use whatever secrets-management your app already has, keyed
under the conventional names most tooling expects:
- Google:
GOOGLE_MAPS_API_KEY - Mapbox:
MAPBOX_ACCESS_TOKEN
e.g. with react-native-config
or Expo's app.config.js → extra field, then pass the resolved value into
the apiKey prop:
import Config from 'react-native-config';
<LiveTrackingMap
routingProvider="google"
apiKey={Config.GOOGLE_MAPS_API_KEY}
...
/>This keeps the component itself provider-agnostic — it never reads environment variables directly, it just takes whatever key you resolve and pass in.
If you're using Google for routing, an unrestricted-enough Google Maps key
can serve both roles (map tiles via the Android setup above, Directions via
this apiKey prop) — they're separate mechanisms reading the same key, not
two keys you have to keep in sync.
Quick start (zero network calls)
import LiveTrackingMap from 'react-native-live-tracking-map';
<View style={{ flex: 1 }}>
<LiveTrackingMap
userLocation={userLocation}
partnerLocation={partnerLocation}
/>
</View>With real roads + traffic
// Google Directions (needs Directions API enabled + billing key)
<LiveTrackingMap
userLocation={userLocation}
partnerLocation={partnerLocation}
routingProvider="google"
apiKey={GOOGLE_MAPS_API_KEY}
showTraffic
showRouteInfoBadge
onRouteInfo={(info) => console.log(info.durationInTrafficSeconds)}
/>
// Mapbox Directions (traffic-aware "driving-traffic" profile)
<LiveTrackingMap
userLocation={userLocation}
partnerLocation={partnerLocation}
routingProvider="mapbox"
apiKey={MAPBOX_ACCESS_TOKEN}
showRouteInfoBadge
/>
// OSRM — free, no API key, no traffic data (good dev/default fallback)
<LiveTrackingMap
userLocation={userLocation}
partnerLocation={partnerLocation}
routingProvider="osrm"
/>
// Bring your own routing backend
<LiveTrackingMap
userLocation={userLocation}
partnerLocation={partnerLocation}
routingProvider="custom"
customRouteFetcher={async (origin, destination, signal) => {
const res = await fetch('https://my-api.com/route', { signal });
const json = await res.json();
return { coordinates: json.points, distanceMeters: json.distance, durationSeconds: json.duration };
}}
/>Driver side
The fastest version — DriverTrackingMap wires the GPS/compass hook into the
map for you:
import { DriverTrackingMap } from 'react-native-live-tracking-map';
<DriverTrackingMap
// Where the driver is headed right now — swap it on status change
destination={isPickedUp ? customerCoord : restaurantCoord}
// Already throttled: fires on 20m of movement OR every 15s
onReportLocation={coords =>
updateCurrentLocation({ latitude: coords.latitude, longitude: coords.longitude })
}
onArrive={() => setCanUpdateStatus(true)}
partnerMarkerImage={DELIVERY_BOY}
userMarkerImage={PIN_LOCATION}
routingProvider="google"
apiKey={Config.GOOGLE_MAPS_API_KEY}
showRouteInfoBadge
showTraffic
/>If you already own the location plumbing (a Redux watcher, a foreground service, Expo Location), skip the wrapper and drive the map directly:
<LiveTrackingMap
mode="driver"
partnerLocation={myLocation} // this device
userLocation={destination}
headingDegrees={compassHeading}
followMode="course-up"
onArrive={() => {}}
/>mode="driver" only changes defaults (followMode: 'course-up',
native flat-marker rotation, a visible navigate button). Every one of them is
still individually overridable, so a hybrid screen is a matter of props.
Using the tracking hook on its own
const { location, heading, isTracking, error, reportNow, stop } = useDriverTracking({
minReportDistanceMeters: 20,
minReportIntervalMs: 15000,
accuracyFilterMeters: 80,
onReport: coords => api.updateCurrentLocation(coords),
onError: e => e.code === 'permission-denied' && promptForSettings(),
});
// Force a report at a moment that matters, regardless of throttling
await reportNow();Multi-stop / waypoints
<LiveTrackingMap
mode="driver"
partnerLocation={driver}
userLocation={finalDrop}
waypoints={[{ coordinate: firstDrop, title: 'Drop 1' }]}
routingProvider="google"
apiKey={KEY}
/>Turn-by-turn handoff
Real voice navigation needs a provider-locked native SDK, which this package deliberately doesn't wrap. What it gives you instead is a one-call handoff:
import { openExternalNavigation } from 'react-native-live-tracking-map';
await openExternalNavigation(destination, { travelMode: 'driving' });
// or via the ref: mapRef.current?.openExternalNavigation()Apple Maps on iOS, Google Maps turn-by-turn on Android, web URL as fallback.
Imperative controls
import { useRef } from 'react';
import LiveTrackingMap, { LiveTrackingMapHandle } from 'react-native-live-tracking-map';
const mapRef = useRef<LiveTrackingMapHandle>(null);
<LiveTrackingMap ref={mapRef} userLocation={userLocation} partnerLocation={partnerLocation} />
mapRef.current?.fitToMarkers();
mapRef.current?.refreshRoute();
// driver side
mapRef.current?.startFollow();
mapRef.current?.stopFollow();
mapRef.current?.isFollowing();
mapRef.current?.getDistanceToDestination(); // metres, straight line
await mapRef.current?.openExternalNavigation();Props
| Prop | Type | Default | Notes |
|---|---|---|---|
| userLocation | LatLng \| null | — | Destination point |
| partnerLocation | LatLng \| null | — | Moving point; update on every poll/socket tick |
| userMarkerImage / partnerMarkerImage | ImageSourcePropType | — | Custom marker icons |
| etaLabel | string | — | Shown under marker titles |
| primaryColor | string | #FF6B35 | Route/polyline color |
| animateMarkerDurationMs | number | 4500 | Match your update interval |
| rotateMarkerToBearing | boolean | true | Rotate partner marker to face travel direction |
| routingProvider | 'google' \| 'mapbox' \| 'osrm' \| 'custom' \| 'none' | 'none' | Real routing backend |
| apiKey | string | — | Required for google / mapbox |
| travelMode | 'driving' \| 'walking' \| 'bicycling' \| 'cycling' | 'driving' | |
| showTraffic | boolean | true | Native traffic layer + traffic-aware duration where supported |
| routeRefreshIntervalMs | number | 30000 | Throttle for re-fetching the route |
| minRerouteDistanceMeters | number | 40 | Skip re-fetch if partner hasn't moved this far |
| customRouteFetcher | (origin, destination, signal?) => Promise<RouteResult> | — | Required when routingProvider="custom" |
| onRouteInfo / onRouteError | callbacks | — | |
| showRouteInfoBadge | boolean | false | Built-in ETA/distance pill |
| routeInfoFormatter | (info: RouteResult) => string | — | Customize the badge text |
Driver-side props
| Prop | Type | Default | Notes |
|---|---|---|---|
| mode | 'customer' \| 'driver' | 'customer' | Changes defaults only |
| headingDegrees | number \| null | — | Compass/GPS heading; overrides derived bearing |
| followMode | 'none' \| 'center' \| 'course-up' | 'none' / 'course-up' in driver mode | Camera behaviour |
| followZoom / followPitch | number | 17 / 45 in course-up | Camera framing |
| autoResumeFollowMs | number | 8000 | Resume following after the user pans; 0 = manual only |
| onFollowSuspended / onFollowResumed | callbacks | — | |
| waypoints | Waypoint[] | — | Intermediate stops, routed through and marked |
| arrivalRadiusMeters | number | 60 | 0 disables arrival detection |
| onArrive | (destination: LatLng) => void | — | Fires once per destination |
| onDistanceToDestination | (meters: number) => void | — | Straight-line readout on every update |
| offRouteThresholdMeters | number | 60 | Distance from the drawn route before rerouting; 0 disables |
| onOffRoute | (meters: number) => void | — | |
| showsUserLocation | boolean | false | Native blue dot |
| mapPadding | {top,right,bottom,left} | — | For bottom sheets covering the map |
| customMapStyle | any[] | — | Google Maps style JSON |
| markerRotationMode | 'native' \| 'image' | 'native' in driver mode | Native flat-marker rotation vs image transform |
| showExternalNavButton | boolean | true in driver mode | Google/Apple Maps handoff button |
| mapViewProps | object | — | Escape hatch passed to MapView |
useDriverTracking(options)
| Option | Default | Notes |
|---|---|---|
| enabled | true | Pause when the screen isn't focused |
| requestPermission | true | Android runtime permission on start |
| distanceFilterMeters | 10 | OS-level update threshold |
| accuracyFilterMeters | 80 | Drop fixes worse than this; 0 disables |
| minReportDistanceMeters | 20 | Report after this much movement... |
| minReportIntervalMs | 15000 | ...or at least this often |
| onReport / onPosition / onError | — | Report is throttled, position is every fix |
| enableCompass | true | Uses react-native-compass-heading if installed |
| geolocationModule | — | Inject your own (Expo Location wrapper, tests) |
Returns { location, position, heading, speed, isTracking, error, start, stop, reportNow }.
Background tracking
This package tracks in the foreground only. Keeping a driver's location
flowing while the app is backgrounded or the screen is off needs a foreground
service (Android) and background location entitlements (iOS) — platform
configuration that belongs in your app, not in a portable component. Wire your
background service's coordinates into partnerLocation and everything else
here keeps working.
Security note
Keep Google/Mapbox API keys restricted (HTTP referrer / bundle ID / IP restrictions) rather than shipping an unrestricted key in your app bundle, same as any client calling a paid maps API directly.
License
MIT
