from permission dialogs to background tracking
A single getCurrentPosition call sounds like the easiest thing in the world, but location services sit at the intersection of accuracy, battery consumption and user privacy. Integrating geolocation into a React Native app without a clear concept risks drained batteries, confused users and rejected App Store reviews. This article shows how to request location services correctly on iOS and Android, track efficiently and test reliably.
Table of Contents
- 1. Why location services need special care
- 2. APIs at a glance: expo-location vs. react-native-geolocation-service
- 3. Requesting permissions correctly
- 4. One-time position lookups with getCurrentPosition
- 5. Continuous tracking with watchPosition
- 6. Background location tracking
- 7. Geofencing
- 8. Accuracy, error handling and testing
- 9. Geolocation approaches compared
- 10. Summary
- 11. FAQ
1. Why location services need special care
Geolocation in a React Native app sounds simple at first: call getCurrentPosition once, display the coordinates, done. In practice, location services sit at the intersection of three competing requirements: accuracy, battery consumption and user privacy. A GPS fix with high accuracy noticeably drains more power than a network-based position estimate, and every continuous background location query adds up to measurable battery loss over the course of a day.
Anyone who integrates geolocation without a clear concept usually notices too late: users complain about drained batteries, or the app fails App Store review because the permission justification is missing or background tracking is requested without a recognizable purpose. Both Apple and Google now actively check whether an app genuinely needs location access for its core purpose, and reject submissions where that connection is not clearly evident.
The following sections show how to implement location services cleanly in React Native: from choosing the right library, through correct permission requests, to background tracking and geofencing. The common thread is always the same: as much accuracy as the given use case requires, never more.
2. APIs at a glance: expo-location vs. react-native-geolocation-service
For geolocation in React Native there are essentially two actively maintained paths. In the Expo managed workflow, expo-location is the obvious choice: it wraps one-time position lookups, continuous tracking and geofencing behind a single, well-documented API, without requiring any manual native module linking.
In the bare workflow, meaning a React Native project without the Expo runtime, react-native-geolocation-service has established itself as the replacement for the geolocation web API polyfill that was eventually removed from React Native core. This community standard offers more fine-grained control over native Android and iOS providers and is actively tested against new OS versions, while the old core implementation had not received updates in years.
The choice between the two depends on the project setup, not on taste: if you already work with Expo, stick with expo-location, because pulling in additional bare libraries unnecessarily complicates the managed workflow. If you have an existing bare project or need very specific native control over GPS providers, react-native-geolocation-service is the better fit. Migrating away from the outdated, web-API-style navigator.geolocation to one of the two libraries is almost always the right call for location services in production apps.
3. Requesting permissions correctly
Location services may only become active on iOS and Android after explicit user consent, and both platforms require a visible justification for it. On iOS, the text for foreground access goes into NSLocationWhenInUseUsageDescription in Info.plist, and for background access additionally into NSLocationAlwaysAndWhenInUseUsageDescription. If either entry is missing, the app crashes on the access attempt instead of returning an error, a behavior that is easy to overlook during development because the crash only occurs on the very first access.
On Android, ACCESS_FINE_LOCATION for precise GPS positions and ACCESS_COARSE_LOCATION for network-based approximations are requested as runtime permissions, meaning at runtime through a dialog, not solely via the manifest declaration. Since Android 6, the manifest declaration is only the prerequisite; the actual consent is obtained via PermissionsAndroid.request() or the equivalent Expo API.
One detail that is frequently overlooked: since Android 10, ACCESS_BACKGROUND_LOCATION must be requested in a separate, second dialog and must not be bundled together with the foreground permissions in one request. Google rejects apps that try to obtain both at once, because the user should only grasp the implications of permanent location access after having already experienced the foreground access. In practice this means: request the foreground permission first, let the user actually use the app, and only ask for the second permission once a concrete background use case comes up.
// LocationTracker.js — request permission then watch position with distanceFilter
import { useEffect, useRef, useState } from 'react';
import { Platform, PermissionsAndroid } from 'react-native';
import Geolocation from 'react-native-geolocation-service';
async function requestLocationPermission() {
if (Platform.OS === 'ios') {
const authStatus = await Geolocation.requestAuthorization('whenInUse');
return authStatus === 'granted';
}
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location access required',
message: 'The app needs your location to track deliveries live.',
buttonPositive: 'Allow',
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
}
export function useLiveLocation() {
const [position, setPosition] = useState(null);
const [error, setError] = useState(null);
const watchId = useRef(null);
useEffect(() => {
let isMounted = true;
requestLocationPermission().then((hasPermission) => {
if (!hasPermission || !isMounted) return;
watchId.current = Geolocation.watchPosition(
(pos) => setPosition(pos.coords),
(err) => setError(err),
{
accuracy: { android: 'high', ios: 'best' },
distanceFilter: 25, // meters — throttle updates to save battery
interval: 5000,
fastestInterval: 2000,
}
);
});
return () => {
isMounted = false;
if (watchId.current !== null) {
Geolocation.clearWatch(watchId.current);
}
};
}, []);
return { position, error };
}
4. One-time position lookups with getCurrentPosition
For a one-time position lookup, getCurrentPosition() is the right method, both in expo-location and in react-native-geolocation-service. The most important options are accuracy, timeout and maximumAge. accuracy controls how precise, and therefore how power-hungry, the position lookup turns out to be, timeout caps the wait time for a fix, and maximumAge defines how old a cached position may be before a new GPS fix is forced.
maximumAge is underused in practice. Many implementations force a fresh GPS fix on every call, even though a position cached ten or twenty seconds ago would be entirely sufficient for the given use case, for example showing the user's approximate position on a map right after app launch. A sensible default for maximumAge usually sits between 10000 and 30000 milliseconds, depending on how time-critical the position needs to be.
The timeout value should be generous enough to still get a fix under weak GPS reception, for example indoors or between tall buildings, but not so high that the UI feels frozen. Ten seconds is a common compromise. If the request fails, the UI should immediately show a clear error state instead of loading indefinitely, since a stuck loading indicator is one of the most common user annoyances with location services.
5. Continuous tracking with watchPosition
Once an application needs to react continuously to position changes, for example live tracking during a delivery run, watchPosition() comes into play. Unlike getCurrentPosition, watchPosition repeatedly delivers new positions for as long as the subscription is active, and must be explicitly stopped when leaving the relevant screen, otherwise tracking keeps running in the background and wastes battery unnecessarily.
The most important lever for battery optimization is distanceFilter. It specifies how many meters the position must have changed at minimum before a new update is triggered. A distanceFilter of 0 delivers every tiny change, which with a naturally noisy GPS signal leads to an unnecessarily large number of updates. A value between 10 and 50 meters drastically reduces the update frequency without compromising the practical usefulness of the tracking.
Accuracy and battery consumption are directly linked in watchPosition: accuracy: 'high' forces GPS and delivers the most precise position, but costs noticeably more power than a network-based estimate via cell towers or Wi-Fi access points. For many use cases, for example roughly showing the user's region for location-based content, a lower accuracy is entirely sufficient and should be chosen deliberately, rather than reflexively requesting the highest accuracy every time.
6. Background location tracking
Background location tracking, meaning location services that keep running even when the app is not in the foreground, requires the "Location updates" Background Modes capability in the Xcode project on iOS. Without this capability, iOS pauses every location query as soon as the app moves to the background, regardless of which permissions were granted beforehand.
Android has become significantly more restrictive since version 10, and even more so since version 12: continuous background tracking requires a foreground service with a permanently visible notification that makes it transparent to the user that the app is actively tracking their location. An attempt to run geolocation in the background without a foreground service is terminated by the system after a short time, regardless of granted permissions.
Two practical paths exist for implementation: react-native-background-geolocation as a complete, commercial solution with built-in foreground service management, batching and offline queueing, or the combination of expo-task-manager and expo-location for simpler use cases within the Expo ecosystem. The latter registers a task that gets woken up periodically by the operating system even when the app is closed, while the former is designed for continuous, high-frequency tracking as used by fitness or delivery apps.
7. Geofencing
Geofencing adds to location services the ability to react to entering or leaving a defined geographic region, without the app having to continuously evaluate the exact position itself. Both iOS and Android offer native region-monitoring APIs for this, which trigger enter and exit events that the operating system itself processes and only forwards to the app on an actual boundary crossing.
This system-side processing is considerably more efficient than periodically polling the position yourself and comparing it against stored coordinates, because the operating system handles the monitoring with adjusted accuracy and minimal energy consumption. Typical use cases are automatic check-in when entering a store, or geofenced notifications, for example a reminder as soon as the user is near a particular branch location.
One practical limit that surprises many developers: iOS monitors a maximum of around 20 regions simultaneously per app. If you need to cover more locations, for example every branch of a nationwide chain, only actively register the nearest 20 regions server-side based on the user's approximate position, and update that list dynamically on larger position changes, instead of trying to monitor every location at once.
{
"expo": {
"name": "mironsoft-demo",
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "This app uses your location to track deliveries live.",
"locationWhenInUsePermission": "This app uses your location to show you nearby content.",
"locationAlwaysPermission": "This app tracks deliveries even in the background.",
"isAndroidBackgroundLocationEnabled": true,
"isIosBackgroundLocationEnabled": true
}
]
],
"ios": {
"infoPlist": {
"UIBackgroundModes": ["location"]
}
},
"android": {
"permissions": [
"ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION",
"ACCESS_BACKGROUND_LOCATION"
]
}
}
}
8. Accuracy, error handling and testing
GPS signals are inherently inaccurate or entirely unavailable indoors, in underground parking garages or between tall buildings, because the signal from satellites gets disrupted by walls and reflections. A robust implementation of location services shows a clear state in such cases, for example "Location could not be determined", instead of silently falling back to a stale or obviously wrong position.
For testing on Android, it is worth looking at mock location detection in developer options: apps with security-critical location use cases should explicitly check whether a position originates from a mock location app, and reject it if necessary to prevent manipulation. For pure development testing, on the other hand, the position can be conveniently simulated in the Android emulator via Extended Controls, including playback of recorded GPS routes.
Under Xcode, locations are simulated via the location simulation in the scheme, either with a fixed coordinate or a predefined route such as a drive through a city. This allows testing watchPosition-based features without actually moving the device. Timeout and permission-denied errors should always be handled differently in the UI: a timeout justifies a retry button, whereas a denied permission warrants a message with a direct link to the system settings.
# Install the two most common React Native geolocation libraries
npm install react-native-geolocation-service
npx expo install expo-location
# iOS Simulator — set a fixed location
xcrun simctl location booted set 52.520008,13.404954
# iOS Simulator — play back a simulated route (walk/run/drive)
xcrun simctl location booted run --speed 20 route-berlin.gpx
# Android Emulator — set location via adb (requires emulator console access)
adb emu geo fix 13.404954 52.520008
# Android Emulator — connect to the console directly for scripted routes
telnet localhost 5554
# then inside the telnet session:
# geo fix 13.404954 52.520008
// LocationManager.swift — minimal background location updates
import CoreLocation
final class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
manager.allowsBackgroundLocationUpdates = true
manager.pausesLocationUpdatesAutomatically = false
}
func start() {
manager.requestAlwaysAuthorization()
manager.startUpdatingLocation()
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
// Forward the coordinate to the React Native bridge event emitter
print("Background update: \(location.coordinate.latitude), \(location.coordinate.longitude)")
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location error: \(error.localizedDescription)")
}
}
// LocationModule.kt — minimal FusedLocationProviderClient usage
import com.google.android.gms.location.*
class LocationModule(private val context: Context) {
private val client = LocationServices.getFusedLocationProviderClient(context)
private val request = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY, 5000L
).setMinUpdateDistanceMeters(25f).build()
private val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
val location = result.lastLocation ?: return
// Forward coordinates to the React Native bridge event emitter
println("Update: ${location.latitude}, ${location.longitude}")
}
}
fun start() {
client.requestLocationUpdates(request, callback, Looper.getMainLooper())
}
fun stop() {
client.removeLocationUpdates(callback)
}
}
9. Geolocation approaches compared
The choice between a one-time position lookup, continuous tracking and true background geolocation is not a matter of style, it depends directly on the use case. The following overview compares the three most common approaches for location services in React Native.
| Approach | Accuracy | Battery usage | Use case | Complexity |
|---|---|---|---|---|
| getCurrentPosition (one-time) | High (GPS) or low (network), depending on the option | Low, single request | One-time position lookup, e.g. on app start | Low |
| watchPosition (continuous, foreground) | High with accuracy: 'high', GPS-based | Medium to high, depending on distanceFilter | Live tracking during active app usage | Medium, subscription management required |
| Background geolocation (library/foreground service) | High, but batch-optimized | High without careful tuning | Tracking even with the app closed, e.g. delivery services | High, foreground service + notification mandatory |
In practice it is best to start with the simplest fitting approach: a one-time position lookup whenever it suffices, watchPosition only while the user is actively interacting with the app, and true background geolocation only when the app's core purpose strictly requires it. This order keeps both battery consumption and implementation effort for location services in check.
Mironsoft
React Native development for iOS and Android
Location services that respect battery and users?
We implement geolocation in your React Native app with correct permissions, battery-conscious tracking and robust background handling, including geofencing and reliable testing on real devices.
Permission flows
Requesting foreground and background permissions correctly and App Store compliant
Battery optimization
distanceFilter, accuracy tiers and foreground service tuning for minimal consumption
Geofencing & testing
Region monitoring, simulated GPS routes and device testing before every release
10. Summary
Location services and geolocation in React Native are not solved with a single API call, they require a deliberate decision per use case. expo-location fits the managed workflow, react-native-geolocation-service fits the bare workflow. Foreground and background permissions must be requested separately and at the right time, both on iOS via the Info.plist entries and on Android via the two-stage runtime dialogs.
getCurrentPosition with a sensible maximumAge covers most one-time lookups, watchPosition with a suitable distanceFilter covers live tracking during active use, and true background geolocation only when the app's core purpose strictly requires ongoing tracking, with a visible foreground service on Android. Geofencing replaces manual position polling with efficient, system-side region monitoring. Combining these building blocks to match your specific use case results in location services that work reliably without sacrificing battery life or user trust.
React Native Geolocation and Location Services: Key Takeaways
Right library
expo-location in the managed workflow, react-native-geolocation-service in the bare workflow instead of outdated web API polyfills.
Permissions in two steps
Foreground access first, ACCESS_BACKGROUND_LOCATION on Android 10+ only afterwards in a separate dialog.
Battery awareness
Actively use maximumAge and distanceFilter, choose accuracy only as high as the use case requires.
Background & geofencing
Foreground service with notification on Android, roughly 20 simultaneously monitored geofencing regions max on iOS.