DEV Community

loading...
Cover image for Integrating MapBox with Next.js — the Cheaper Alternative to Google Maps

Integrating MapBox with Next.js — the Cheaper Alternative to Google Maps

niharikak101 profile image Niharika Khanna ・6 min read

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,
map

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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>);
}
Enter fullscreen mode Exit fullscreen mode

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>);
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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
  });
......
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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:

map

Discussion (4)

pic
Editor guide
Collapse
gsarig profile image
Giorgos Sarigiannidis

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.

Collapse
haksatryabhaswara profile image
haksatryabhaswara

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?

Collapse
samthomson profile image
Sam Thomson

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).

Collapse
crishrpz profile image
crishrpz

Awesome!