Lately, I’ve been working on creating an online platform for eco hotels and resorts, and found myself needing to render a map with some clickable marker pins (which we are going to reproduce in this tutorial). After scouring the internet with possible solutions, two immediate options sprung up — Google Maps and MapBox. While most of us are familiar with Google Maps because of the overwhelming presence it has in all our lives, integrating it in an application, I found, is less than ideal. Building a bootstrapped project, I wanted to keep the costs at a minimum and Google Map’s pricing structure would mean that the costs would begin to add up.
Enter MapBox!
With a competitive pricing structure (the first 50,000 requests on web are free) and an easy-to-use and well documented API, MapBox is a good alternative to Google Maps. It is also built on top of OpenStreetMap, which is an open source mapping project. Win, win!
What are we building?
We’re going to be querying MapBox’s search api to get some locations of an infamous coffee shop called Greggs, focusing our search on the Greater London region. Then, we are going to render these places in our MapBox <Map>
component with a bunch of clickable markers. On click, these markers will display some dismissible popups.
The finished product will look something like,
Let's Code!
Make a MapBox account to get your access token
The first thing you will need to do is to make a MapBox account so that you can get an access token. We will use this token to make requests to the various MapBox APIs.
Once you have your access token, it is time to set up your very own Next.js project and integrate all the juicy functionality that MapBox provides.
Setup a new Next.js project (Skip this if you already have a project of your own)
Setting up a Next.js project is straightforward, you can either follow the instructions laid out in the official documentation or run the following command to set up a new Next.js project (Make sure you have Node.js installed).
npx create-next-app mapbox-project
Then, cd
into the mapbox-project
directory and run the development server by running npm run dev
or yarn dev
. Et Voila! Your Next.js project is up and running!
Setup A MapBox Map
Next up, it’s time to render a MapBox map in our project. We do this by adding a MapBox library written by the team at Uber called react-map-gl. This contains a suite of React components for MapBox. Add this library to your project by running:
yarn add react-mapbox-gl
With this, we’re going to create a Map component which will live in components/Map.js
.
Create your Map.js file and add the following code to it:
import { useState } from "react";
import ReactMapGL from "react-map-gl";
export default function Map() {
const [viewport, setViewport] = useState({
width: "100%",
height: "100%",
// The latitude and longitude of the center of London
latitude: 51.5074,
longitude: -0.1278,
zoom: 10
});
return <ReactMapGL
mapStyle="mapbox://styles/mapbox/streets-v11"
mapboxApiAccessToken={process.env.MAPBOX_KEY}
{...viewport}
onViewportChange={(nextViewport) => setViewport(nextViewport)}
>
</ReactMapGL>
}
This is not going to work just yet. One of the biggest features of Next.js is the server side rendering it offers. MapBox, however, requires the global window object in order to work correctly. If you are server side rendering your app, you will need to dynamically import it into your page. This means that instead of importing it like a regular component,
import Map from '../components/Map'
We will have to import it dynamically. We will do this by using Next.js dynamic imports
.
In your pages/index.js
file (or wherever you’re rendering your Map component) add the following code.
const Map = dynamic(() => import("../components/Map"), {
loading: () => "Loading...",
ssr: false
});
This means that our MapBox component will now selectively be rendered client side. Perfect!
The only thing we need to do now is to add MapBox’s CSS files to our project. The easiest way to do this is to modify your existing _app.js
or by adding a custom _document.js
file. Then add a link to the CSS to the <Head>
in your render function. You can get the latest version of the CSS files in their API documentation.
<head>
<link href='https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css' rel='stylesheet' />
</head>
Perfect. Your Map should now be up and running! Let’s take this a step further and try rendering some clickable pins on our map.
Use MapBox’s search API to fetch a list of landmarks
MapBox has a really handy geocoding API which you can be used to fetch a list of locations, with their latitudes and longitudes. We’re going to be fetching a list of Greggs (a take-away fast food and coffee shop) in London and render them as pins on our Map.
First, let’s query our list by adding a simple fetch call to the Mapbox geocoding API. We want to search within the geographic bounds of London and want to cap our search at 10 results (London is huge and Londoner’s love their Gregg’s vegan sausage rolls. We don’t want to overwhelm ourselves with all the possibilities!). MapBox’s Geocoding Place Search API takes the following parameters, with some additional query strings.
/geocoding/v5/mapbox.places/{search_text}.json
We will be using the limit query parameter to cap our results at 10, and the bbox parameter to specify the latitudinal and longitudinal bounds of London.
With all this in mind, our search url will look something like this:
https://api.mapbox.com/geocoding/v5/mapbox.places/greggs.json?access_token=${process.env.MAPBOX_KEY}&bbox=-0.227654%2C51.464102%2C0.060737%2C51.553421&limit=10
We can use this url, to make a simple fetch call in our page. Our modified page will now look something like,
const Map = dynamic(() => import("../components/Map"), {
loading: () => "Loading...",
ssr: false
});
const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/greggs.json?access_token=${process.env.MAPBOX_KEY}&bbox=-0.227654%2C51.464102%2C0.060737%2C51.553421&limit=10`;
export default function IndexPage() {
const [locations, setLocations] = useState([]);
useEffect(() => {
const fetchLocations = async () => {
await fetch(url).then((response) =>
response.text()).then((res) => JSON.parse(res))
.then((json) => {
setLocations(json.features);
}).catch((err) => console.log({ err }));
};
fetchLocations();
}, []);
return (<Container>
<Map />
</Container>);
}
We now have a list of 10 Greggs locations!
Using our search results to render pins on our map
Now that we have a list of places, we can render these on a Map. react-map-gl
comes with a handy <Marker>
component that makes our task pretty straight forward. First we need to pass these locations to our <Map>
component.
return (<Container>
<Map locations={locations} />
</Container>);
Now, within out Map component, we need to render a pin for each of these locations by passing their latitude and longitude to the <Marker>
component.
Our final Map component will look something like,
import { useState } from "react";
import ReactMapGL, { Marker } from "react-map-gl";
export default function Map({ locations }) {
const [viewport, setViewport] = useState({
width: "100%",
height: "100%",
// The latitude and longitude of the center of London
latitude: 51.5074,
longitude: -0.1278,
zoom: 10
});
return <ReactMapGL
mapStyle="mapbox://styles/mapbox/streets-v11"
mapboxApiAccessToken={process.env.MAPBOX_KEY}
{...viewport}
onViewportChange={(nextViewport) => setViewport(nextViewport)}
>
{locations.map((location) => (
<div key={location.id}>
<Marker
latitude={location.center[1]}
longitude={location.center[0]}
offsetLeft={-20}
offsetTop={-10}>
<span role="img" aria-label="push-pin">📌</span>
</Marker>
</div>
))}
</ReactMapGL>
}
Making the pins clickable
We’re almost there! The last thing we want to do to make these maps fully functioning and interactive, is to add a popup with the name of the place. Again, Mapbox comes with a handy Popup component that makes this easy to do. We will simply add an onClick handler to our pins which will capture the details of the selected location, then we will pass the latitude and the longitude of the selected location to our <Popup>
component. It’ll all be clear in a second!
Within the Map component, add a useState
hook to capture the selected location.
export default function Map({ locations }) {
// UseState hook
const [selectedLocation, setSelectedLocation] = useState({})
const [viewport, setViewport] = useState({
width: "100%",
height: "100%",
// The latitude and longitude of the center of London
latitude: 51.5074,
longitude: -0.1278,
zoom: 10
});
......
We will also modify the render block to add an onClick handler and the <Popup>
component that we just mentioned.
......
return <ReactMapGL
mapStyle="mapbox://styles/mapbox/streets-v11"
mapboxApiAccessToken={process.env.MAPBOX_KEY}
{...viewport}
onViewportChange={(nextViewport) => setViewport(nextViewport)}
>
{locations.map((location) => (
<div key={location.id}>
<Marker
latitude={location.center[1]}
longitude={location.center[0]}
offsetLeft={-20}
offsetTop={-10}>
<a onClick={() => {
setSelectedLocation(location);
}}>
<span role="img" aria-label="push-pin">📌</span>
</a>
</Marker>
{selectLocation.id === location.id ? (
<Popup
onClose={() => setSelectedLocation({})}
closeOnClick={true}
latitude={location.center[1]}
longitude={location.center[0]}>
{location.place_name}
</Popup>) : (false)}
</div>
))}
</ReactMapGL>
}
The <Popup>
component takes an onClose handler which sets the selectedLocation to {}.
And that is all! We’ve managed to render a map, rendered some pins on our map and managed to make them clickable with popups! Here’s our final result:
Top comments (8)
I suggest to change the mapboxApiAccessToken param in the example with mapboxAccessToken as it is supported atm. (Maybe was changed in the past?)
But thanks a lot for this neat example!
Another change: selectLocation.id in the example code needs to be selectedLocation.id. Sorry for the pickiness ;)
A few months ago I made an OpenStreetMap Gutenberg Block for WordPress, which has some resemblance in regards to the code, as it is mostly React as well. In my case, Leaflet.js (react-leaflet in particular) was very handy to build the overall experience. Its advantage is that its service-agnostic and it can easily be extended to use other Tile Providers, like Google Maps, Here Maps, or whatever.
I get the error 'ReferenceError: dynamic is not defined'
This was after I changed the required package from react-mapbox-gl to react-map-gl (since that is what you import from).
'ReferenceError: dynamic is not defined' is because u need to import dynamic from
"next/dynamic"
Thanks. In the end I removed
react-mapbox-gl
which I found restrictive and just usedmapbox-gl
itself. No issues so far and easy to make use of everything mapbox offers.Good to know that, buddy 👍👍
Hey i really like your explaination! i've tried it and it works, but i've one question, did you know how to prevent mapbox component to be re-render after i changed state in parent component?
Awesome!