Real-Time Geolocation Tracking with React Native

Real-Time Geolocation Tracking with React Native social.jpeg

At PubNub, we believe in powering the global frontier of remote interactions. Whether it be the burgeoning paradigm of IoT, the global growth of chat apps, or the relentless expansion of online applications, our mission extends to Internet technologies of all shapes and sizes. Using PubNub's Geolocation APIs to track your fleet’s cars, check on your drivers’ deliveries, provision orders, or even matchmake in dating apps. Whether you want to build a rideshare app, on-demand delivery service, or a Pokemon Go game, you can depend on PubNub's scalability and reliability.

In this tutorial, you'll learn how easy it is to build real-time geolocation tracking with PubNub and React Native, one of the most popular mobile app frameworks. The geolocation application renders multiple users on a map view, allowing users to toggle their location permissions as well as click a button to zoom the map on their location. You'll also be able to determine how many users are currently online in the app using Presence.

Although this tutorial guides you step-by-step to create the geolocation React Native app, you can find the final source code of the application in the GitHub repository and read our article to learn more about geolocation APIs.

Environment Set up

In this section, you will install the necessary tools and dependencies to be able to simulate, run, and test the React Native application.

PubNub Account

You'll need to create a free PubNub account, as you'll need your publish/subscribe keys to send information across the PubNub Network. You can learn how to create your keys in this video/written walkthrough.

Ensure that you enable Presence to know when users come online and Message Persistence to persist information.

Development Environments

You will need to download and use your favorite code editor when developing this application. Visual Studio Code is recommended as it is a lightweight, open-source, and free code editor that supports different languages and frameworks. You should also add the React Native Tools plugin for debugging and integrated commands for React Native.

The next set of tools you’ll need for developing cross-platform applications is to download iOS and Android development environments. For iOS devices, you'll need to download Xcode. For Mac users, you can simply download Xcode for free in the app store. For PC users, you will need to simulate Mac OS with a Virtual Machine if you want to develop your app for iPhone. You can see how to do this here. For simulating Android applications, you'll need to download Android Studio.

Packages and Libraries

Download Node.js, the JavaScript runtime environment, as it is necessary to run React Native applications, as well as install packages with npm (which is included with the download).

Next, you'll install React Native. This is an open-source platform developed by Facebook that has become very popular over the years. React Native allows developers to write their applications in one language across multiple platforms, which allows you to develop for Android and iOS devices.

To set up the React Native development environment, you'll be using the React Native CLI, which will allow you to quickly install libraries, link packages, and simulate the app.

Navigate in the terminal/console to the folder you would like to develop this application. Use npm to install the React Native CLI command line utility.

sudo npm install -g react-native-cli
sudo npm install -g react-native

Note: You will be prompted to install CocoaPods after this installation. Say yes to this download and make sure you don’t run CocoaPods as root (i.e. make sure you installed react-native first using sudo and then you can use react-native as a normal user. CocoaPods then should run properly).

Then run the following commands to create a new React Native project called YourProject.

react-native init YourProject
cd YourProject

To see if everything’s working properly, run this command to test your app with Xcode’s iPhone simulator.

sudo react-native run-ios

If you’re having issues with the react-native run-ios command, try building the project in Xcode. Make sure to specify iPhone X as the simulator.

Although you will need to add some additional logic for the application to be compatible with Android devices, you can run the project in the Android emulator with the following command.

react-native run-android

Once you've ensured that everything is properly working, you are going to import and link PubNub’s React SDK to be able to communicate across the PubNub network. In your project directory, install the PubNub React SDK and link the library using the following commands.

npm install --save pubnub pubnub-react@1
react-native link pubnub-react

Next, you'll need the GitHub react-native-maps library made by Airbnb for the interactive map API. Install and link the library.

npm install react-native-maps --save
react-native link react-native-maps

Finally, install and link the react-native-responsive package, which will make styling components easier.

npm install --save react-native-responsive-screen
react-native link react-native-responsive-screen

Build and Run

Now it’s time to finally start building the geolocation application with real-time updates.

Importing Libraries with React Native

In the project directory, open up the App.js file and import the libraries that were installed earlier along with some basic React Native components using the following code.

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View, TouchableOpacity, Switch, Image} from 'react-native';
import {widthPercentageToDP as wp, heightPercentageToDP as hp} from 'react-native-responsive-screen';
import MapView, {Marker} from 'react-native-maps';
import PubNubReact from 'pubnub-react';

Constructor and State Variables

In the same App.js file, initialize a constructor to pass in the props as well as initialize a PubNub instance.

constructor(props) {
  super(props);

  this.pubnub = new PubNubReact({
    publishKey: "YOUR PUBLISH KEY",
    subscribeKey: "YOUR SUBSCRIBE KEY"
  });

  this.pubnub.init(this);
}

A PubNub instance is created with the Publish and Subscribe API keys from your PubNub account and then initialized at the end of the constructor. It is very important that the PubNub instance is initialized at the end of the constructor as the code will not work if done otherwise.

Next, create some state variables that you will need while the app is running. If you haven’t used state in React yet, it may be useful to read up about state in React Native before moving on.

constructor(props) {
  super(props);

  this.pubnub = new PubNubReact({
    publishKey: "YOUR PUBLISH KEY",
    subscribeKey: "YOUR SUBSCRIBE KEY"
  });

  //Base State
  this.state = {
    currentLoc: { //Track user's current location
      latitude: -1,
      longitude: -1
    },
    numUsers: 0, //track number of users on the app
    username: "A Naughty Moose", //user's username
    fixedOnUUID: "",
    focusOnMe: false, //zoom map to user's current location if true
    users: new Map(), //store data of each user in a Map
    isFocused: false, 
    allowGPS: true, //toggle the app's ability to gather GPS data of the user
  };

  this.pubnub.init(this);
}

While most of the state variables seem fairly intuitive for a Geotracking app, the users map requires further explanation.

The users map facilitates how multiple users are rendered in the app. Each entry in the map will represent one user and will map to the specific in-app data the user contains (GPS coordinates, UUID, allowGPS, etc). PubNub is then used to publish JSON data updates from each user to update the mapping and re-render the application’s state variables accordingly.

For example, if you want to update a user’s allowGPS property, you publish a JSON object to update that user’s variable mapping.

this.pubnub.publish({
  message: {
    hideUser: true
  },
  channel: "channel"
});

Once the state variables are initialized, you'll declare an asynchronous function in ComponentDidMount.

async componentDidMount() {
  this.setUpApp()
}

In order to start receiving PubNub messages within the app, you need to declare a PubNub event listener followed by a PubNub subscriber callback, specifying the channel.

  async setUpApp(){

    this.pubnub.getMessage("YOUR CHANNEL", msg => {
      
    
/*------------WE'LL IMPLEMENT THIS LATER------------*/

   
    });

    this.pubnub.subscribe({
      channels: ["YOUR CHANNEL"],
    });
  }

This function will be fully implemented later in the tutorial.

React Native Maps

You will now start implementing the interactive map for your users as well as track their GPS data.

To collect a user’s position, implement the react-native-maps watchPosition function below the PubNub subscriber.

//Track motion Coordinates
navigator.geolocation.watchPosition(
  position => {
    this.setState({
      currentLoc: position.coords
    });
    if (this.state.allowGPS) {
      this.pubnub.publish({
        message: {
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
        },
        channel: "channel"
      });
    }
    //console.log(positon.coords);
  },
  error => console.log("Maps Error: ", error),
  {
    enableHighAccuracy: true,
    distanceFilter: 100 //grab the location whenever the user's location changes by 100 meters
  }
);

You should now be able to see the logic for the user map framework. After collecting the geohash position coordinates, the latitude and longitude data are published to the channel. The channel will later update this user’s location data based on the publisher’s UUID. Just like any other map application, a function is added to center the map on the user’s location if a button is pressed. To add this functionality, add this function implementation.

focusLoc = () => {
   if (this.state.focusOnMe || this.state.fixedOnUUID) {
     this.setState({
       focusOnMe: false,
       fixedOnUUID: ""
     });
   } else {
     region = {
       latitude: this.state.currentLoc.latitude,
       longitude: this.state.currentLoc.longitude,
       latitudeDelta: 0.01,
       longitudeDelta: 0.01
     };
     this.setState({
       focusOnMe: true
     });
     this.map.animateToRegion(region, 2000);
   }
 }

When called, this function will center the map’s viewing region on the user’s current location. If you want the user to have the ability to turn off their GPS location, the allowGPS state needs to be toggled. To do this, add the following function.

toggleGPS = () => {
   this.setState({
     allowGPS: !this.state.allowGPS
   });
 };

User Data Aggregation

Navigate back to the PubNub event listener that was defined earlier. The purpose of the event listener for this app is to take the data updates published to the channel and update the state variables.

In order to update the users map, initialize a copy of the mapping to manipulate.

this.pubnub.getMessage("channel", msg => {
  let users = this.state.users;
});

Inside the event listener, determine if the incoming message is a request from a user to hide their GPS data and remove them from the mapping accordingly.

if (msg.message.hideUser) {
  users.delete(msg.publisher);
  this.setState({
    users
  });
}else{
/*something else*/
}

Otherwise, the message contains data updates for a user and a new user (with the updated values) must be initialized to replace the old one.

else{
        coord = [msg.message.latitude, msg.message.longitude]; //Format GPS Coordinates for Payload

        let oldUser = this.state.users.get(msg.publisher);

        let newUser = {
          uuid: msg.publisher,
          latitude: msg.message.latitude,
          longitude: msg.message.longitude,
        };

        if(msg.message.message){
          Timeout.set(msg.publisher, this.clearMessage, 5000, msg.publisher);
          newUser.message = msg.message.message;
        }else if(oldUser){
          newUser.message = oldUser.message
        }
        users.set(newUser.uuid, newUser);

        this.setState({
          users
        });
}

You have just finished implementing the receiving end of updating a user’s data. You will now implement the sending end where the user will actually publish messages with their data updates.

In order to know when a user has changed one of their data variables, an event handler is used to detect these changes. React’s componentDidUpdate function is used, which triggers anytime there is a change in the app’s data.

Specify the componentDidUpdate function to pass in the previous props and state. Check whether the user has toggled their allowGPS and focusOnMe variables and make the necessary changes to the app’s function and state.

componentDidUpdate(prevProps, prevState) {
   if (prevState.allowGPS != this.state.allowGPS) { //check whether the user just toggled their GPS settings
  if (this.state.allowGPS) { //if user toggled to show their GPS data, we add them to the user Map once again
    if (this.state.focusOnMe) { //if user toggled to focus map view on themselves
      this.animateToCurrent(this.state.currentLoc, 1000);
    }
    let users = this.state.users; //make a copy of the users array to manipulate
    
    //create a new user object with updated user values to replace the old user
    let tempUser = {
      uuid: this.pubnub.getUUID(),
      latitude: this.state.currentLoc.latitude,
      longitude: this.state.currentLoc.longitude,
      image: this.state.currentPicture,
      username: this.state.username
    };
    users.set(tempUser.uuid, tempUser);
    this.setState( //quickly update the user Map locally
      {
        users
      },
      () => {
        this.pubnub.publish({ //publish updated user to update everyone's user Map
          message: tempUser,
          channel: "channel"
        });
      }
    );
  } else { //if user toggled to hide their GPS data
    let users = this.state.users;
    let uuid = this.pubnub.getUUID();

    users.delete(uuid); //delete this user from the user Map
    this.setState({ //update the userMap
      users,
    });
    this.pubnub.publish({ //let everyone else's user Map know this user wants to be hidden
      message: {
        hideUser: true
      },
      channel: "channel"
    });
  }
}
}

You may have noticed a redundancy in this code snippet. Why is the state of the updated user being set locally and then publishing the updated user object to the channel? Isn’t that setting the state twice?

While the assumption is correct, there is a reason behind this action. The state is first updated locally so you can update the user’s screen as fast as possible for accurate location tracking. Then, the updated user object is published to the channel so that everyone else on the network can update their state as well.

Rendering

You'll now implement the UI of the application in the render function of the App.js file. First, implement the user map as a usable array.

let usersArray = Array.from(this.state.users.values());

Inside the return, render the map component from React-Native-Maps by setting the initial region with some starting coordinates. You'll then iterate through the map to begin rendering every user on the network, and for each user, you'll render a marker component from React-Native-Maps as well as an image to represent that user.

return (
     <View style={styles.container}  >
          <MapView
            style={styles.map}
            ref={ref => (this.map = ref)}
            onMoveShouldSetResponder={this.draggedMap}
            initialRegion={{
              latitude: 36.81808,
              longitude: -98.640297,
              latitudeDelta: 60.0001,
              longitudeDelta: 60.0001
            }}
          >
{usersArray.map((item) => (
<Marker
  style={styles.marker}
  key={item.uuid} //distinguish each user's marker by their UUID
  coordinate={{ //user's coordinates 
    latitude: item.latitude,
    longitude: item.longitude
  }}
  ref={marker => {
    this.marker = marker;
  }}
>
  <Image
      style={styles.profile}
      source={require('./LOCATION OF YOUR USER IMAGE PROFILES')} //User's image 
  />
</Marker>))}
          </MapView>
     </View>
);

Below the MapView, you can define a toggle switch for the user to toggle their allowGPS state.

<View style={styles.topBar}>
  <View style={styles.rightBar}>
      <Switch
      value={this.state.allowGPS}
      style={styles.locationSwitch}
      onValueChange={this.toggleGPS}
      />
  </View>
</View>

Finally, add a button to enter the map on the user.

<View style={styles.bottom}>
<View style={styles.bottomRow}>   
  <TouchableOpacity onPress={this.focusLoc}>
    <Image style={styles.focusLoc} source={require('./heart.png')} />
  </TouchableOpacity>
</View>
</View>

Styling

Add the following to style your components.

const styles = StyleSheet.create({
  bottomRow:{
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center"
  },
  marker: {
    justifyContent: "center",
    alignItems: "center",
    marginTop: Platform.OS === "android" ? 100 : 0,
  },
  topBar: {
    top: Platform.OS === "android" ? hp('2%') : hp('5%'),

    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    marginHorizontal: wp("2%"),
  },
  rightBar: {
    flexDirection: "row",
    justifyContent: "flex-end",
    alignItems: "center"
  },
  leftBar: {
    flexDirection: "row",
    justifyContent: "flex-start",
    alignItems: "center"
  },
  locationSwitch: {
    left: 300,
  },
  container: {
    flex: 1
  },
  bottom: {
    position: "absolute",
    flexDirection:'column',
    bottom: 0,
    justifyContent: "center",
    alignSelf: "center",
    width: "100%",
    marginBottom: hp("4%"),
  },
  focusLoc: {
    width: hp("4.5%"),
    height: hp("4.5%"),
    marginRight: wp("2%"),
    left: 15
  },
  userCount: {
    marginHorizontal: 10
  },
  map: {
    ...StyleSheet.absoluteFillObject
  },
  profile: {
    width: hp("4.5%"),
    height: hp("4.5%")
  },
});

Android Compatibility

For Android devices to be compatible with this application, add the following in the beginning of the setUpApp function.

let granted;

if (Platform.OS === "android"){
  granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION ,
    {
      title: 'Location Permission',
      message:
        'PubMoji needs to access your location',
      buttonNegative: 'No',
      buttonPositive: 'Yes',
    });      
}

You will also need to add an if statement around the watchPosition as follows.

if (granted === PermissionsAndroid.RESULTS.GRANTED || Platform.OS === "ios") { /*-----watchPosition()----*/ } 
    else {
      console.log( "ACCESS_FINE_LOCATION permission denied" )
    }

Running the Geolocation Application

You are now ready to run the application. You can simulate the app with iOS devices using the following command.

sudo react-native run-ios

For Android devices, you can simulate the application with the following command.

react-native run-android

What's Next

Congratulations, you have created your own real-time geolocation app in React Native using PubNub! You're able to render multiple users on a map view, allow users to click a button to zoom the map on their location, and enable users to toggle their location permissions.

You can check out the completed version of the application on GitHub. If you would like to learn more about how to power your React applications with PubNub, be sure to check out PubNub's other React developer resources:

If you have any other questions or concerns, please feel free to reach out to devrel@pubnub.com.