Location tracking in Expo and React Native
December 27, 2021
In recent years, on-demand services such as taxi and food delivery apps have become very popular and useful. One of the features behind this success, besides many other factors, is the real-time position tracking of their users.
In this guide, we’ll explain how to track position in React Native even if the app is in the background, using two packages expo-location and expo-task-manager.
Getting started
💡 If you’re new to React Native, this example will get you started quickly with Expo.
To use the expo-location
package, it must first be installed.
$ expo install expo-location
If you’re in a bare React Native project, you should also follow these additional installation instructions.
Then we import the package in our app:
import * as Location from "expo-location"
User location data is considered sensitive information, that’s why we need to ask permission otherwise, tracking won’t even start.
const foregroundPermission = await Location.requestForegroundPermissionsAsync()
Executing Location.requestForegroundPermissionsAsync()
will display permission modal to the user and the foregroundPermission
object varies depending on user action, it will have a key granted
with the value true or false.
Then we use Location.watchPositionAsync(options, callback)
to track the location when the app is in use.
👉 Check all available options here: https://docs.expo.dev/versions/latest/sdk/location/#locationoptions
👉 Callback is a function with location
param that has coords
with the following format: https://docs.expo.dev/versions/latest/sdk/location/#locationobjectcoords
// Location subscription in the global scope
let locationSubscrition = null
// Locatoin tracking inside the component
if (foregroundPermission.granted) {
foregroundSubscrition = Location.watchPositionAsync(
{
// Tracking options
accuracy: Location.Accuracy.High,
distanceInterval: 10,
},
location => {
/* Location object example:
{
coords: {
accuracy: 20.100000381469727,
altitude: 61.80000305175781,
altitudeAccuracy: 1.3333333730697632,
heading: 288.87445068359375,
latitude: 36.7384213,
longitude: 3.3463877,
speed: 0.051263172179460526,
},
mocked: false,
timestamp: 1640286855545,
};
*/
// Do something with location...
}
)
}
Then if we want to stop the location tracking we call the subscriber remove
method this way:
locationSubscrition?.remove()
Location tracking in the background
The previous explanation only shows how to track the foreground location, once the user switches to another app or locks their phone, the location tracking stops.
To enable position tracking in the background we need to add some configurations to our app.config.json
file.
👉 In a bare React Native project check Task manager documentation.
Android permissions will be added automatically when the app is in use, but it’s better to specify all permissions before sending the app to the store.
Here’s config I use:
{
"expo": {
...
"ios": {
...
"infoPlist": {
...
"UIBackgroundModes": [
"location",
"fetch"
]
}
},
"android": {
...
"permissions": [
...
'ACCESS_BACKGROUND_LOCATION'
]
}
}
}
Before we track position in background we need to get permission in both foreground and background:
if (!foregroundPermission.granted) return
const backgroundPermission = await Location.getBackgroundPermissionsAsync()
Then we will use Location.startLocationUpdatesAsync
, but this method requires a task working in the background, that’s why we need to install expo-task-manager
.
$ expo install expo-task-manager
After installing and configuring the task manager, we need to define the location tracking task in the global scope:
Import the task manager:
import * as TaskManager from "expo-task-manager"
Here’s how to define the task globally:
const TASK_NAME = "BACKGROUND_LOCATION_TASK"
TaskManager.defineTask(TASK_NAME, async ({ data, error }) => {
if (error) {
console.error(error);
return;
}
if (data) {
/* Data object example:
{
locations: [
{
coords: {
accuracy: 22.5,
altitude: 61.80000305175781,
altitudeAccuracy: 1.3333333730697632,
heading: 0,
latitude: 36.7384187,
longitude: 3.3464008,
speed: 0,
},
timestamp: 1640286402303,
},
],
};
*/
const { locations } = data;
const location = locations[0];
if (location) {
// Do something with location...
}
}
});
export default function App() {...}
Inside the app or any child component, we call Location.startLocationUpdatesAsync(taskName, options)
method, this latter will trigger the defined task and run the callback.
👉 All available options: https://docs.expo.dev/versions/latest/sdk/location/#locationtaskoptions
if (backgroundPermission.status === "granted") {
Location.startLocationUpdatesAsync(TASK_NAME, {
// The following notification options will help keep tracking consistent
showsBackgroundLocationIndicator: true,
foregroundService: {
notificationTitle: "Location",
notificationBody: "Location tracking in background",
notificationColor: "#fff",
},
})
}
Let’s not forget to stop the location tracking, for example if the user logs out, just by calling the following method:
Location.stopLocationUpdatesAsync(TASK_NAME)
Example
The following example will clarify the tracking process.
The app will show four buttons to start and stop location tracking when the app is in use or in the background. When the app starts, it will ask you to grant position tracking permissions in the foreground and background.
import React, { useEffect, useState } from "react"
import { StyleSheet, Text, View, Button } from "react-native"
import * as TaskManager from "expo-task-manager"
import * as Location from "expo-location"
const LOCATION_TASK_NAME = "LOCATION_TASK_NAME"
let foregroundSubscription = null
// Define the background task for location tracking
TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error(error)
return
}
if (data) {
// Extract location coordinates from data
const { locations } = data
const location = locations[0]
if (location) {
console.log("Location in background", location.coords)
}
}
})
export default function App() {
// Define position state: {latitude: number, longitude: number}
const [position, setPosition] = useState(null)
// Request permissions right after starting the app
useEffect(() => {
const requestPermissions = async () => {
const foreground = await Location.requestForegroundPermissionsAsync()
if (foreground.granted) await Location.requestBackgroundPermissionsAsync()
}
requestPermissions()
}, [])
// Start location tracking in foreground
const startForegroundUpdate = async () => {
// Check if foreground permission is granted
const { granted } = await Location.getForegroundPermissionsAsync()
if (!granted) {
console.log("location tracking denied")
return
}
// Make sure that foreground location tracking is not running
foregroundSubscription?.remove()
// Start watching position in real-time
foregroundSubscription = await Location.watchPositionAsync(
{
// For better logs, we set the accuracy to the most sensitive option
accuracy: Location.Accuracy.BestForNavigation,
},
location => {
setPosition(location.coords)
}
)
}
// Stop location tracking in foreground
const stopForegroundUpdate = () => {
foregroundSubscription?.remove()
setPosition(null)
}
// Start location tracking in background
const startBackgroundUpdate = async () => {
// Don't track position if permission is not granted
const { granted } = await Location.getBackgroundPermissionsAsync()
if (!granted) {
console.log("location tracking denied")
return
}
// Make sure the task is defined otherwise do not start tracking
const isTaskDefined = await TaskManager.isTaskDefined(LOCATION_TASK_NAME)
if (!isTaskDefined) {
console.log("Task is not defined")
return
}
// Don't track if it is already running in background
const hasStarted = await Location.hasStartedLocationUpdatesAsync(
LOCATION_TASK_NAME
)
if (hasStarted) {
console.log("Already started")
return
}
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
// For better logs, we set the accuracy to the most sensitive option
accuracy: Location.Accuracy.BestForNavigation,
// Make sure to enable this notification if you want to consistently track in the background
showsBackgroundLocationIndicator: true,
foregroundService: {
notificationTitle: "Location",
notificationBody: "Location tracking in background",
notificationColor: "#fff",
},
})
}
// Stop location tracking in background
const stopBackgroundUpdate = async () => {
const hasStarted = await Location.hasStartedLocationUpdatesAsync(
LOCATION_TASK_NAME
)
if (hasStarted) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME)
console.log("Location tacking stopped")
}
}
return (
<View style={styles.container}>
<Text>Longitude: {position?.longitude}</Text>
<Text>Latitude: {position?.latitude}</Text>
<View style={styles.separator} />
<Button
onPress={startForegroundUpdate}
title="Start in foreground"
color="green"
/>
<View style={styles.separator} />
<Button
onPress={stopForegroundUpdate}
title="Stop in foreground"
color="red"
/>
<View style={styles.separator} />
<Button
onPress={startBackgroundUpdate}
title="Start in background"
color="green"
/>
<View style={styles.separator} />
<Button
onPress={stopBackgroundUpdate}
title="Stop in foreground"
color="red"
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
switchContainer: {
flexDirection: "row",
alignItems: "center",
},
button: {
marginTop: 15,
},
separator: {
marginVertical: 8,
},
})
Notes:
👉 Location tracking in the background is not always consistent, it depends on the OS and the configuration of the device.
👉 If you plan to submit your app to Google Play, you should notify the user, with a modal showing that you are using their location in the background before requesting location tracking.