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.
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]
}
}]
}
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!
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>
);
}
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.
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>
);
}
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.
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>
);
}
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"
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>
);
}
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>
))}
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>
);
}
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.