ReactT Light_870x220

Knowing the similarities and differences between Google Maps and MapBox within a React app will help you pick the right tool for the job. This article compares and contrasts these two popular libraries, getting you up and running with whichever one you choose.

Airbnb, Uber, Realtor, and so many other websites provide a map view of their data. Unsurprisingly, it's the easiest way to visualize geographic data, which many apps have. A problem arises, though, when you read the documentation for the two most popular mapping libraries: Google Maps and MapBox. You won't find documentation for how to easily use them within React, the most popular frontend framework.

In this article, we will see how to display data on a map within React, showing examples with both Google Maps and MapBox. The final Google Maps version and MapBox version can be found here. If you'd like to follow along with a video, check out the Google Maps and MapBox videos I have posted.

We Need Data

Ottawa, the capital of Canada, has a great set of open data for their city. In this example, we will be working with data showing where all the city's skateparks are. The entire JSON file can be found here, but I have stripped out the fields we aren't using to show a small sample of what it looks like.

The most important thing, and a requirement to place anything on a map, is a location's latitude and longitude. In the example below, the coordinates property has an array where the longitude is the first element, and latitude is the second.

{
  "features": [{
    "properties": {
      "PARK_ID": 960,
      "NAME": "Bearbrook Skateboard Park",
      "DESCRIPTION": "Flat asphalt surface, 5 components"
    },
    "geometry": {
      "coordinates": [-75.3372987731628, 45.383321536272049]
    }
  }, {
    "properties": {
      "PARK_ID": 1219,
      "NAME": "Bob MacQuarrie Skateboard Park (SK8 Extreme Park)",
      "DESCRIPTION": "Flat asphalt surface, 10 components, City run learn to skateboard programs, City run skateboard camps in summer"
    },
    "geometry": {
      "coordinates": [-75.546518086577947, 45.467134581917357]
    }
  }, {
    "properties": {
      "PARK_ID": 1157,
      "NAME": "Walter Baker Skateboard Park",
      "DESCRIPTION": "Concrete bowl, 7,000 sq ft"
    },
    "geometry": {
      "coordinates": [-75.898610599532319, 45.295014379864874]
    }
  }]
}

React and Google Maps

We will be using a React library called react-google-maps to help us integrate React with Google Maps. After installing it, the next thing we need to do is grab an API key. This can be done within the Google Developer Console. You should be fine with a free account as long as it's just a personal project or for a demo. Make sure to enable the Maps JavaScript API for your project.

Rather than placing our API Key inside our code, let's use an environment variable to make it available. In create-react-app, environment variables beginning with REACT_APP_ are automatically made available. We'll place it in a file called .env.local, making sure to include it in the .gitignore file.

REACT_APP_GOOGLE_KEY="your-api-code-here"

We'll come back to this API Key later. For now, let's get started building our map!

Getting Started with Google Maps

The first component we will build is the Map component. Its purpose is to render the data inside of the GoogleMap component, which comes from the package we installed. It doesn't require any initial props, but passing in the zoom level and where to center the map are pretty typical.

import { GoogleMap } from "react-google-maps";

function Map() {
  return (
    <GoogleMap defaultZoom={10} defaultCenter={{ lat: 45.4211, lng: -75.6903 }}>
      { /* We will render our data here */ }
    </GoogleMap>
  );
}

Adding Data to Google Maps

With the Map component rendering the GoogleMap, it's time to put some data inside of it. We are importing our data from a local JSON file, but you could just as easily load it from a remote API within a useEffect hook when the component is mounted. The idea is to loop through each of the skateparks, rendering a Marker for each one.

import { GoogleMap, Marker } from "react-google-maps";
import * as parkData from "./data/skateboard-parks.json";

function Map() {
  return (
    <GoogleMap
      defaultZoom={10}
      defaultCenter={{ lat: 45.4211, lng: -75.6903 }}
    >
      {parkData.features.map(park => (
        <Marker
          key={park.properties.PARK_ID}
          position={{
            lat: park.geometry.coordinates[1],
            lng: park.geometry.coordinates[0]
          }}
          icon={{
            url: `/skateboarding.svg`,
            scaledSize: new window.google.maps.Size(25, 25)
          }}
        />
      ))}
    </GoogleMap>
  );
}

The key prop is always necessary when you are mapping an array in React, while the position says where to place it. The icon prop isn't necessary, but it lets you override the typical red marker with something custom of your own.

Handling Clicks in Google Maps

With all of our markers being shown, we can now handle when the user clicks one of them. What we're going to do is use some state (with useState) to know which marker was clicked, showing its details inside of an InfoWindow popup.

An onClick prop has been added to each Marker, setting that park as the selectedPark in state. Below the markers, we check if there is a selectedPark, and if so, show an InfoWindow with all of the details of the selected park. This component also requires a position, and an onCloseClick prop to know what to do when the user closes it.

import React, { useState } from "react";
import { GoogleMap, Marker, InfoWindow } from "react-google-maps";
import * as parkData from "./data/skateboard-parks.json";

function Map() {
  const [selectedPark, setSelectedPark] = useState(null);
  return (
    <GoogleMap
      defaultZoom={10}
      defaultCenter={{ lat: 45.4211, lng: -75.6903 }}
    >
      {parkData.features.map(park => (
        <Marker
          key={park.properties.PARK_ID}
          position={{
            lat: park.geometry.coordinates[1],
            lng: park.geometry.coordinates[0]
          }}
          onClick={() => { setSelectedPark(park); }}
          icon={{
            url: `/skateboarding.svg`,
            scaledSize: new window.google.maps.Size(25, 25)
          }}
        />
      ))}

      {selectedPark && (
        <InfoWindow
          onCloseClick={() => { setSelectedPark(null); }}
          position={{
            lat: selectedPark.geometry.coordinates[1],
            lng: selectedPark.geometry.coordinates[0]
          }}
        >
          <div>
            <h2>{selectedPark.properties.NAME}</h2>
            <p>{selectedPark.properties.DESCRIPTION}</p>
          </div>
        </InfoWindow>
      )}
    </GoogleMap>
  );
}

Displaying the Map

We're almost there! The last step is to use this Map component. For that we have to use two HOCs (Higher Order Components) which hook our Map up to Google Maps' JavaScript scripts.

import {
  // existing imports
  withGoogleMap,
  withScriptjs
} from "react-google-maps";

// Map Component Here  

const MapWrapped = withScriptjs(withGoogleMap(Map));

export default function App() {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <MapWrapped
        googleMapURL={`https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=geometry,drawing,places&key=${
          process.env.REACT_APP_GOOGLE_KEY
        }`}
        loadingElement={<div style={{ height: `100%` }} />}
        containerElement={<div style={{ height: `100%` }} />}
        mapElement={<div style={{ height: `100%` }} />}
      />
    </div>
  );
}

The MapWrapped component needs to be inside of a div that has some dimensions. Mine takes up the whole screen (100vh and 100vw). We're required to pass it the googleMapURL, which includes the API Key mentioned earlier, along with three elements which are used internally by the package.

Applying Some Styles to Google Maps

If the standard styles are too boring for you, head on over to Snazzy Maps and grab the JS for your favorite style. This can be passed to the GoogleMap component using the defaultOptions prop. I have put all of these styles into a file called mapStyles.js, which exports them as default.

import mapStyles from "./mapStyles";

function Map() {
  return (
    <GoogleMap
      defaultZoom={10}
      defaultCenter={{ lat: 45.4211, lng: -75.6903 }}
      defaultOptions={{ styles: mapStyles }}
    >
      { /* Markers and InfoWindow here */ }
    </GoogleMap>
  );
}

React and MapBox

For MapBox we will be using the react-map-gl package made by the team at Uber. MapBox also requires an Access Token, which can be created for free on the MapBox website. We'll put the Access Token inside of the .env.local file:

REACT_APP_MAPBOX_TOKEN="your-token-here"

Getting Started with MapBox

After having just finished showing how Google Maps works in React, I think you'll find that MapBox is slightly easier. They do have a number of differences though, one being that Google Maps controls its own position (where the user has dragged the map to, zoomed in or out, etc.), whereas with MapBox it is up to us to track all of these details inside some state that we will call the viewport.

After providing the viewport some initial values such as latitude, longitude and zoom, MapBox has a prop called onViewportChange, which is called with the new viewport, based on the user's actions. It's up to us to update the state, which will cause the map to re-render its new position, since we are passing viewport {...viewport} to the map.

Please note that we had to provide the mapboxApiAccessToken. You may also notice that there is a mapStyle prop. Styles can be found by grabbing the Style URL from any of the styles here.

import React, { useState, useEffect } from "react";
import ReactMapGL, { Marker, Popup } from "react-map-gl";
import * as parkDate from "./data/skateboard-parks.json";

export default function App() {
  const [viewport, setViewport] = useState({
    latitude: 45.4211,
    longitude: -75.6903,
    width: "100vw",
    height: "100vh",
    zoom: 10
  });

  return (
    <div>
      <ReactMapGL
        {...viewport}
        mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
        mapStyle="mapbox://styles/leighhalliday/cjufmjn1r2kic1fl9wxg7u1l4"
        onViewportChange={viewport => { setViewport(viewport); }}
      >
        { /* Markers and Popup will go here */ }
      </ReactMapGL>
    </div>
  );
}

Showing Data in MapBox

With the map set up, it's time to display some data. This is very similar to how we handled it in the Google Maps example. We are going to map (no pun intended) the skateparks, creating a Marker for each one. You'll notice that with MapBox you have to provide all of the UI for the Marker by styling a button, adding an image inside of it, or however it should be rendered within your own application. MapBox is very flexible this way.

Notice there is a click handler on the button. This will be used to determine which skatepark to show the details of in the section below. The following code goes inside the ReactMapGL component:

{parkDate.features.map(park => (
  <Marker
    key={park.properties.PARK_ID}
    latitude={park.geometry.coordinates[1]}
    longitude={park.geometry.coordinates[0]}
  >
    <button
      className="marker-btn"
      onClick={e => {
        e.preventDefault();
        setSelectedPark(park);
      }}
    >
      <img src="/skateboarding.svg" alt="Skate Park Icon" />
    </button>
  </Marker>
))}

Handling Clicks in MapBox

We have already rendered the map along with all of its Markers. Now it's time to handle displaying a skatepark's details when its Marker has been clicked. We will set up some state called selectedPark, which will be set in the onClick prop of each Marker.

There is first a check to see if selectedPark has a value, and, if it does, a Popup component is rendered. Popup requires the latitude and longitude as props, along with an onClose click handler that sets the state back to null. Inside of a Popup you can place any HTML that you would like to display to the user.

import React, { useState, useEffect } from "react";
import ReactMapGL, { Marker, Popup } from "react-map-gl";
import * as parkDate from "./data/skateboard-parks.json";

export default function App() {
  const [viewport, setViewport] = useState({
    latitude: 45.4211,
    longitude: -75.6903,
    width: "100vw",
    height: "100vh",
    zoom: 10
  });

  const [selectedPark, setSelectedPark] = useState(null);

  return (
    <div>
      <ReactMapGL
        {...viewport}
        mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
        mapStyle="mapbox://styles/leighhalliday/cjufmjn1r2kic1fl9wxg7u1l4"
        onViewportChange={viewport => { setViewport(viewport); }}
      >
        { /* Markers here */ }

        {selectedPark ? (
          <Popup
            latitude={selectedPark.geometry.coordinates[1]}
            longitude={selectedPark.geometry.coordinates[0]}
            onClose={() => { setSelectedPark(null); }}
          >
            <div>
              <h2>{selectedPark.properties.NAME}</h2>
              <p>{selectedPark.properties.DESCRIPTION}</p>
            </div>
          </Popup>
        ) : null}
      </ReactMapGL>
    </div>
  );
}

Conclusion

In this article we covered how to integrate the two most popular map libraries into our React app. With these skills we're now ready to be the next unicorn startup! OK OK, without getting carried away, many apps need to display their data on a map, and knowing how to do so in either of these libraries is a great skill to have. Even though Google Maps and MapBox have some differences, the main ideas are the same: Render markers for each location, and handle click events in order to display details about the location that the user has clicked on.


leigh-halliday
About the Author

Leigh Halliday

Leigh Halliday is a full-stack developer specializing in React and Ruby on Rails. He works for FlipGive, writes on his blog, and regularly posts coding tutorials on YouTube.

Related Posts

Comments

Comments are disabled in preview mode.