Real-time Location Tracking with React Native and PubNub

Vikrant Negi
Share

With ever-increasing usage of mobile apps, geolocation and tracking functionality can be found in a majority of apps. Real-time geolocation tracking plays an important role in many on-demand services, such as these:

  • taxi services like Uber, Lyft or Ola
  • food Delivery services like Uber Eats, Foodpanda or Zomato
  • monitoring fleets of drones

In this guide, we’re going use React Native to create a real-time location tracking app. We’ll build two React Native apps. One will act as a tracking app (called “Tracking app”) and the other will be the one that’s tracked (“Trackee app”).

Here’s what the final output for this tutorial will look like:

Want to learn React Native from the ground up? This article is an extract from our Premium library. Get an entire collection of React Native books covering fundamentals, projects, tips and tools & more with SitePoint Premium. Join now for just $9/month.

Prerequisites

This tutorial requires a basic knowledge of React Native. To set up your development machine, follow the official guide here.

Apart from React Native, we’ll also be using PubNub, a third-party service that provides real-time data transfer and updates. We’ll use this service to update the user coordinates in real time.

Register for a free PubNub account here.

Since we’ll be using Google Maps on Android, we’ll also need a Google Maps API key, which you can obtain on the Google Maps Get API key page.

To make sure we’re on the same page, these are the versions used in this tutorial:

  • Node v10.15.0
  • npm 6.4.1
  • yarn 1.16.0
  • react-native 0.59.9
  • react-native-maps 0.24.2
  • pubnub-react 1.2.0

Getting Started

If you want to have a look at the source code of our Tracker and Trackee apps right away, here are their GitHub links:

Let’s start with the Trackee app first.

Trackee App

To create a new project using react-native-cli, type this in the terminal:

$ react-native init trackeeApp
$ cd trackeeApp

Now let’s get to the fun part — the coding.

Add React Native Maps

Since we’ll be using Maps in our app, we’ll need a library for this. We’ll use react-native-maps.

Install react-native-maps by following the installation instructions here.

Add PubNub

Apart from maps, we’ll also install the PubNub React SDK to transfer our data in real time:

$ yarn add pubnub-react

After that, you can now run the app:

$ react-native run-ios
$ react-native run-android

You should see something like this on your simulator/emulator:

Trackee App

Trackee Code

Now, open the App.js file and the following imports:

import React from "react";
import {
  StyleSheet,
  View,
  Platform,
  Dimensions,
  SafeAreaView
} from "react-native";
import MapView, { Marker, AnimatedRegion } from "react-native-maps";
import PubNubReact from "pubnub-react";

Apart from MapView, which will render the Map in our component, we’ve imported Marker and AnimatedRegion from react-native-mas.

Marker identifies a location on a map. We’ll use it to identify user location on the map.

AnimatedRegion allows us to utilize the Animated API to control the map’s center and zoom.

After importing the necessary component, we’ll define some constants and initial values for our Maps:

const { width, height } = Dimensions.get("window");

const ASPECT_RATIO = width / height;
const LATITUDE = 37.78825;
const LONGITUDE = -122.4324;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;

Then, we’ll define our class component with some state, lifecycle methods and custom helper methods:

export default class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      latitude: LATITUDE,
      longitude: LONGITUDE,
      coordinate: new AnimatedRegion({
        latitude: LATITUDE,
        longitude: LONGITUDE,
        latitudeDelta: 0,
        longitudeDelta: 0
      })
    };

    this.pubnub = new PubNubReact({
      publishKey: "X",
      subscribeKey: "X"
    });
    this.pubnub.init(this);
  }

  componentDidMount() {
    this.watchLocation();
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.props.latitude !== prevState.latitude) {
      this.pubnub.publish({
        message: {
          latitude: this.state.latitude,
          longitude: this.state.longitude
        },
        channel: "location"
      });
    }
  }

  componentWillUnmount() {
    navigator.geolocation.clearWatch(this.watchID);
  }

  watchLocation = () => {
    const { coordinate } = this.state;

    this.watchID = navigator.geolocation.watchPosition(
      position => {
        const { latitude, longitude } = position.coords;

        const newCoordinate = {
          latitude,
          longitude
        };

        if (Platform.OS === "android") {
          if (this.marker) {
            this.marker._component.animateMarkerToCoordinate(
              newCoordinate,
              500 // 500 is the duration to animate the marker
            );
          }
        } else {
          coordinate.timing(newCoordinate).start();
        }

        this.setState({
          latitude,
          longitude
        });
      },
      error => console.log(error),
      {
        enableHighAccuracy: true,
        timeout: 20000,
        maximumAge: 1000,
        distanceFilter: 10
      }
    );
  };

  getMapRegion = () => ({
    latitude: this.state.latitude,
    longitude: this.state.longitude,
    latitudeDelta: LATITUDE_DELTA,
    longitudeDelta: LONGITUDE_DELTA
  });

  render() {
    return (
      <SafeAreaView style={{ flex: 1 }}>
        <View style={styles.container}>
          <MapView
            style={styles.map}
            showUserLocation
            followUserLocation
            loadingEnabled
            region={this.getMapRegion()}
          >
            <Marker.Animated
              ref={marker => {
                this.marker = marker;
              }}
              coordinate={this.state.coordinate}
            />
          </MapView>
        </View>
      </SafeAreaView>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: "flex-end",
    alignItems: "center"
  },
  map: {
    ...StyleSheet.absoluteFillObject
  }
});

Whew! That’s a lot of code, so let’s walk through it bit by bit.

First, we’ve initialized some local state in our constructor(). We’ll also initialize a PubNub instance:

constructor(props) {
  super(props);

  this.state = {
    latitude: LATITUDE,
    longitude: LONGITUDE,
    coordinate: new AnimatedRegion({
      latitude: LATITUDE,
      longitude: LONGITUDE,
      latitudeDelta: 0,
      longitudeDelta: 0,
    }),
  };

  // Initialize PubNub
  this.pubnub = new PubNubReact({
    publishKey: 'X',
    subscribeKey: 'X',
  });

  this.pubnub.init(this);
}

You’ll need to replace “X” with your own PubNub publish and subscribe keys. To get your keys, log in to your PubNub account and go to the dashboard.

You’ll find a Demo Project app already available there. You’re free to create a new app, but for this tutorial we’ll use this Demo project.

Copy and Paste the keys in the PubNub constructor instance.

After that, we’ll use the componentDidMount() Lifecycle to call the watchLocation method:

componentDidMount() {
  this.watchLocation();
}

watchLocation = () => {
  const { coordinate } = this.state;

  this.watchID = navigator.geolocation.watchPosition(
    position => {
      const { latitude, longitude } = position.coords;

      const newCoordinate = {
        latitude,
        longitude,
      };

      if (Platform.OS === 'android') {
        if (this.marker) {
          this.marker._component.animateMarkerToCoordinate(newCoordinate, 500); // 500 is the duration to animate the marker
        }
      } else {
        coordinate.timing(newCoordinate).start();
      }

      this.setState({
        latitude,
        longitude,
      });
    },
    error => console.log(error),
    {
      enableHighAccuracy: true,
      timeout: 20000,
      maximumAge: 1000,
      distanceFilter: 10,
    }
  );
};

The watchLocation uses the geolocation API to watch changes in user’s location coordinates. So any time the user moves and his position coordinates changes, watchPosition will return the user’s new coordinates.

The watchPosition accepts two parameters—options and callback.

As options, we’ll set enableHighAccuracy to true for high accuracy, and distanceInterval to 10 to receive updates only when the location has changed by at least ten meters in distance. If you want maximum accuracy, use 0, but be aware that it will use more bandwidth and data.

In the callback, we get the position coordinates and we call use these coordinates to set the local state variables.

const { latitude, longitude } = position.coords;

this.setState({
  latitude,
  longitude
});

Now that we have the user coordinates, we’ll use them to add a marker on the map and then update that marker continuously as the user coordinates changes with its position.

For this, we’ll use animateMarkerToCoordinate() for Android and coordinate.timing() for iOS. We’ll pass an object newCoordinate with latitude and longitude as a parameter to these methods:

if (Platform.OS === "android") {
  if (this.marker) {
    this.marker._component.animateMarkerToCoordinate(newCoordinate, 500); // 500 is the duration to animate the marker
  }
} else {
  coordinate.timing(newCoordinate).start();
}

We also want the user’s coordinates to be sent continuously to our Tracker app. To achieve this, we’ll use React’s componentDidUpdate lifecycle method:

 componentDidUpdate(prevProps, prevState) {
  if (this.props.latitude !== prevState.latitude) {
    this.pubnub.publish({
      message: {
        latitude: this.state.latitude,
        longitude: this.state.longitude,
      },
      channel: 'location',
    });
  }
}

The componentDidUpdate is invoked immediately after the updating occurs. So it will be called each time the user’s coordinates get changed.

We’ve further use an if condition to publish the coordinates only when the latitude is changed.

We then called the PubNub publish method to publish the coordinates, along with the channel name location we want to publish those coordinates.

Note: make sure the channel name is the same in both the apps. Otherwise, you won’t receive any data.

Now that we’re done with all the required methods, let’s render our MapView. Add this code in your render method:

return (
  <SafeAreaView style={{ flex: 1 }}>
    <View style={styles.container}>
      <MapView
        style={styles.map}
        showUserLocation
        followUserLocation
        loadingEnabled
        region={this.getMapRegion()}
      >
        <Marker.Animated
          ref={marker => {
            this.marker = marker;
          }}
          coordinate={this.state.coordinate}
        />
      </MapView>
    </View>
  </SafeAreaView>
);

We’ve used Marker.Animated, which will move in an animated manner as users moves and their coordinates change.

componentWillUnmount() {
  navigator.geolocation.clearWatch(this.watchID);
}

We’ll also clear all geolocation watch method in componentWillUnmount() to avoid any memory leaks.

Let’s finish the Trackee app by adding some styles:

const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: "flex-end",
    alignItems: "center"
  },
  map: {
    ...StyleSheet.absoluteFillObject
  }
});

Since we want our map to cover the whole screen, we have to use absolute positioning and set each side to zero (position: 'absolute', left: 0, right: 0, top: 0, bottom: 0).

StyleSheet provides absoluteFill that can be used for convenience and to reduce duplication of these repeated styles.

Running the Trackee App

Before we go any further, it’s always a good idea to test our app. We can do so by taking the following steps.

On iOS

If you’re using iOS simulator, you’re in luck. It’s very easy to test this feature in iOS compared to Android.

In your iOS simulator settings, go to Debug > Location > Freeway Drive and refresh your app (Cmd + R). You should see something like this:

Trackee App

On Android

Unfortunately for Android, there’s no straightforward way of testing this feature.

You can use third-party apps to imitate GPS location apps. I found GPS Joystick to be of great help.

You can also use Genymotion, which has a utility for simulating the location.

Testing on PubNub

To test if PubNub is receiving data, you can turn on Real-time Analytics, which will show the number of messages your app is receiving or sending.

In your Keys tab, go to the bottom and turn on Real-time Analytics. Then go to Real-time Analytics to the check if the data is being received.

This is all the Trackee app needs to do, so let’s move on to the Tracker app.

Tracker App

Follow the same steps as we did for Trackee app and create a new React Native project called trackerApp.

Both Tracker and Trackee apps share the majority their code.

The only difference is that in trackerApp we’ll be getting the location coordinates from the trackeeApp via PubNub.

Add the pubnub-react SDK, import and initialize as we did in the Trackee app.

In componentDidMount(), add the following:

// same imports as trackeeApp

componentDidMount() {
  /* remove
    watchLocation = () => {}
  */

 // add:
  this.subscribeToPubNub();
}

// add:
subscribeToPubNub = () => {
  this.pubnub.subscribe({
    channels: ['location'],
    withPresence: true,
  });
  this.pubnub.getMessage('location', msg => {
    const { coordinate } = this.state;
    const { latitude, longitude } = msg.message;
    const newCoordinate = { latitude, longitude };

    if (Platform.OS === 'android') {
      if (this.marker) {
        this.marker._component.animateMarkerToCoordinate(newCoordinate, 500);
      }
    } else {
      coordinate.timing(newCoordinate).start();
    }

    this.setState({
      latitude,
      longitude,
    });
  });
};

/* remove
watchLocation = () => {
}
*/

Here is the sneak peak of the updated code for the Tracker app.

In the code above, we’re using PubNub’s subscribe method to subscribe to our location channel as soon the component gets mounted.

After that, we’re using getMessage to get the messages received on that channel.

We’ll use these coordinates to update the MapView of the Tracker app.

Since both the apps share the same set of coordinates, we should be able to see the Trackee app coordinates in the Tracker app.

Running Both Apps Together

Finally we’re at the last step. It’s not straightforward to test both apps on the same machine under development mode.

To test both the apps on an iOS machine, I’m going to follow these steps:

  1. We’re going to run the Trackee app on the iOS simulator since, it has the debug mode where I can simulate a moving vehicle. I’m also going to run it on release mode, as we can’t have two packages running at the same time:

     $ react-native run-ios --configuration Release
    

    Now, go to Debug > Location > Freeway Drive.

  2. We’ll run the Tracker app on Android emulator:

     $ react-native run-android
    

The Tracker app now should be able to se the Marker moving just like in the Trackee app.

You can find the source code for both apps on GitHub.

Conclusion

This is just a very basic implementation of Real-time Location Tracking services. We’re just scratching the surface with what we can achieved with location tracking. In reality, the possibilities are endless. For example:

  • You could create a ride-hailing service like Uber, Lyft etc.
  • Using Location tracking, you could track your orders like food or grocery from the local seller.
  • You could track the location of your children (useful for parents or teachers).
  • You could track animals in a protected national park.

If you use this to create your own implementation of location tracking, I’d love to see the results. Let me know on Twitter.