← Home

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.


Chafik Gharbi Full-stack web and mobile app developer with JavaScript.