Using mapbox-gl in React with Next.js
Tags: react, nextjs, typescript, mapbox
Published: 2021-07-01 12:41:01

## Introduction

In this article I want to describe the ways I know of embedding `mapbox-gl` in a`React` application, using the example of creating a simple web application containing a map on `Next.js` using `Typescript`, the map component code can also be used in any `React` application

This article is part of a series of articles

[Managing mapbox-gl state in React app](https://dev.to/dqunbp/managing-mapbox-gl-state-in-react-app-4328)

I will consider several implementation options using the example of creating a functional map component:

- Implementation with keep the map instance inside the `React` component
- Keeping the map instance outside of `React`

## Code snippets info

> For comfortable reading of this article, you need to have 
> basic knowledge of `React`,`Typescript` and `CSS`

> All code snippets will be using `Typescript`, using typing
> in javascript is the best practice, so I basically stick to 
> it where possible, I apologize if you are not familiar with
> it, [here is a great course from egghead.io where you can read it](https://www.notion.so/Using-mapbox-gl-in-React-with-Next-js-f058c0d6e6cb4f5eae9b80c86891f62d#fff64bcf83ac41a7871692ec443b83d2)

>I prefer to import `React` as `import * as React from 
>"react"` you can read more about this in
>[great article by Kent C. Dodds](https://epicreact.dev/importing-react-through-the-ages)

> If `// ...` is encountered in the code, it must be read as
> places with missing duplicate code

## Preparing the environment

First of all, let's create a new project in `Next.js` using the `Typescript` template.

```bash
npx create-next-app --typescript my-awesome-app
```

Let's open the project folder and install the `mapbox-gl` with types for `Typescript`

```bash
cd my-awesome-app

npm install --save mapbox-gl && npm install -D @type/mapbox-gl
```

We also need [accessToken](https://docs.mapbox.com/help/getting-started/access-tokens/) for `mapbox-gl`, from environment variable so as not to store it directly in the source code

```bash
touch .env.local
echo NEXT_PUBLIC_MAPBOX_TOKEN=<your_token> >> .env.local
```

This is how your file should look like with [environment variable for `Next.js`](https://nextjs.org/docs/basic-features/environment-variables)

**.env.local**

```bash
NEXT_PUBLIC_MAPBOX_TOKEN=<your_token>
```

## Implementation as a functional `React` component

### Preparing styles

Remove unnecessary styles and update the global stylesheet

```bash
rm styles/Home.module.css
```

**styles / global.css**

```css
html,
body,
#__next {
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}

* {
  box-sizing: border-box;
}
```

To make the height of the application equal to `100%` of the window height, set the properties `width` and `height` to `100%` for `html` and `body`

The height must also be specified for the element with the `css` selector`#__ next` because in the `Next.js` application the root element is`<div id = "__ next"> ... </div>`

### Preparing a map component

**components/mapbox-map.tsx**

```tsx
import * as React from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css"; 
// import the mapbox-gl styles so that the map is displayed correctly

function MapboxMap() {
	// this is where the map instance will be stored after initialization
  const [map, setMap] = React.useState<mapboxgl.Map>();

	// React ref to store a reference to the DOM node that will be used
  // as a required parameter `container` when initializing the mapbox-gl
  // will contain `null` by default
	const mapNode = React.useRef(null);

  React.useEffect(() => {
    const node = mapNode.current;
		// if the window object is not found, that means
		// the component is rendered on the server
		// or the dom node is not initialized, then return early
    if (typeof window === "undefined" || node === null) return;

		// otherwise, create a map instance
    const mapboxMap = new mapboxgl.Map({
      container: node,
			accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
			style: "mapbox://styles/mapbox/streets-v11",
      center: [-74.5, 40],
      zoom: 9,
    });

		// save the map object to React.useState
    setMap(mapboxMap);
		
		return () => {
      mapboxMap.remove();
    };
  }, []);

	return <div ref={mapNode} style={{ width: "100%", height: "100%" }} />;
}

export default MapboxMap
```

Description of the `mapbox-gl` init parameters can be found in the documentation

[Map | Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addlayer)

Next, we import it to the main page of the application and launch the project

**pages/index.tsx**

```jsx
import MapboxMap from "../components/mapbox-map";

function App() {
  return <MapboxMap />;
}

export default App;
```

```bash
npm run dev
```

Opening [http://localhost:3000](http://localhost:3000) we see a full-screen web map

![fullscreen map image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17yk7728mwfvr0hs1gnd.png)
  

### What Can Be Done Better

The proposed implementation is missing several useful features.

- **Map initialization parameters** - when using a map component, it seems useful to be able to pass initial map options through the `props`
- **Access to the map instance from other components** - the application usually contains other components for which you need to have access directly to the map instance
- **Map ready callback** - loading the map takes some time, while the user is waiting for the opening of the map, to improve the user experience, you can show a skeleton or loading screen with a spinner. For these purposes, it would be convenient to have a callback triggered after the map is fully loaded.

An example with loading a map in my application [https://app.mapflow.ai](https://app.mapflow.ai/)

![map-loading-spinner](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/acqo0seu1j1ndn5ooi2x.gif) 

![map-loading-spinner-zoom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdsyx1sc04qqezxlpxcl.png)

 

## Improving map component

Let's implement all these features, first add the `props` for the `MapboxMap` component

The `container` property of the `MapboxOptions` interface is not required in this case, to exclude it we use [the utility type `Omit`](https://www.typescriptlang.org/docs/handbook/utility-types.html)

Let's pass `initialOptions` to the web map init options using [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals), we will also set a callback for the map `load` event

```tsx
// ...
    const mapboxMap = new mapboxgl.Map({
      container: node,
      accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
      style: "mapbox://styles/mapbox/streets-v11",
      center: [-74.5, 40],
      zoom: 9,
      ...initialOptions,
    });

    setMap(mapboxMap);

		// if onMapLoaded is specified it will be called once
    // by "load" map event
    if (onMapLoaded) mapboxMap.once("load", onMapLoaded);

		// removing map object and calling onMapRemoved callback
    // when component will unmout 
		return () => {
      mapboxMap.remove();
      if (onMapRemoved) onMapRemoved();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

// ...
```

Here you can see a special comment for the linter 

```tsx
// eslint-disable-next-line react-hooks/exhaustive-deps
```

According to `react-hooks/exhaustive-deps` rule we had to specify in the list of dependencies for `React.useEffect` variables added to the hook `[initialOptions, onMapLoaded]`

In this case, it is important to leave dependency list empty, this will allow you not to re-create the map instance if `initialOptions` or `onMapLoaded` was changed, you can read more about using `React.useEffect` at the link below

[A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/)

Final component version will look like this

**components/mapbox-map.tsx**

```tsx
import * as React from "react";
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";

interface MapboxMapProps {
  initialOptions?: Omit<mapboxgl.MapboxOptions, "container">;
  onMapLoaded?(map: mapboxgl.Map): void;
	onMapRemoved?(): void;
}

function MapboxMap({ initialOptions = {}, onMapLoaded }: MapboxMapProps) {
  const [map, setMap] = React.useState<mapboxgl.Map>();

  const mapNode = React.useRef(null);

  React.useEffect(() => {
    const node = mapNode.current;

    if (typeof window === "undefined" || node === null) return;

    const mapboxMap = new mapboxgl.Map({
      container: node,
      accessToken: process.env.NEXT_PUBLIC_MAPBOX_TOKEN,
      style: "mapbox://styles/mapbox/streets-v11",
      center: [-74.5, 40],
      zoom: 9,
      ...initialOptions,
    });

    setMap(mapboxMap);

    if (onMapLoaded) mapboxMap.once("load", onMapLoaded);

		return () => {
      mapboxMap.remove();
      if (onMapRemoved) onMapRemoved();
    };

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return <div ref={mapNode} style={{ width: "100%", height: "100%" }} />;
}

export default MapboxMap;
```

Now we can override the initial map properties and use the `onMapLoaded` callback when it is loaded. We can also use `onMapLoaded` to store a link to the map instance in the parent component, for example. We can also use `onMapRemoved` if we need to know that the map instance has been removed.

We will use this, to define the coordinates of the center of the map, and also add the initial screen for loading the map.

First, let's prepare a `MapLoadingHolder` component that will be displayed on top of the map until it is loaded.

Let's use a `svg` icon for the loading screen. I have it from [https://www.freepik.com](https://www.freepik.com/), and then converted it to `jsx` format using [https://svg2jsx.com/](https://svg2jsx.com/)

**components/world-icon.tsx**

```tsx
function WorldIcon({ className = "" }: { className?: string }) {
  return (
    <svg
      className={className}
      xmlns="http://www.w3.org/2000/svg"
      width="48.625"
      height="48.625"
      x="0"
      y="0"
      enableBackground="new 0 0 48.625 48.625"
      version="1.1"
      viewBox="0 0 48.625 48.625"
      xmlSpace="preserve"
    >
      <path d="M35.432 10.815L35.479 11.176 34.938 11.288 34.866 12.057 35.514 12.057 36.376 11.974 36.821 11.445 36.348 11.261 36.089 10.963 35.7 10.333 35.514 9.442 34.783 9.591 34.578 9.905 34.578 10.259 34.93 10.5z"></path>
      <path d="M34.809 11.111L34.848 10.629 34.419 10.444 33.819 10.583 33.374 11.297 33.374 11.76 33.893 11.76z"></path>
      <path d="M22.459 13.158l-.132.34h-.639v.33h.152l.022.162.392-.033.245-.152.064-.307.317-.027.125-.258-.291-.06-.255.005z"></path>
      <path d="M20.812 13.757L20.787 14.08 21.25 14.041 21.298 13.717 21.02 13.498z"></path>
      <path d="M48.619 24.061a24.552 24.552 0 00-.11-2.112 24.165 24.165 0 00-1.609-6.62c-.062-.155-.119-.312-.185-.465a24.341 24.341 0 00-4.939-7.441 24.19 24.19 0 00-1.11-1.086A24.22 24.22 0 0024.312 0c-6.345 0-12.126 2.445-16.46 6.44a24.6 24.6 0 00-2.78 3.035A24.18 24.18 0 000 24.312c0 13.407 10.907 24.313 24.313 24.313 9.43 0 17.617-5.4 21.647-13.268a24.081 24.081 0 002.285-6.795c.245-1.381.379-2.801.379-4.25.001-.084-.004-.167-.005-.251zm-4.576-9.717l.141-.158c.185.359.358.724.523 1.094l-.23-.009-.434.06v-.987zm-3.513-4.242l.004-1.086c.382.405.75.822 1.102 1.254l-.438.652-1.531-.014-.096-.319.959-.487zM11.202 7.403v-.041h.487l.042-.167h.797v.348l-.229.306h-1.098l.001-.446zm.778 1.085s.487-.083.529-.083 0 .486 0 .486l-1.098.069-.209-.25.778-.222zm33.612 9.651h-1.779l-1.084-.807-1.141.111v.696h-.361l-.39-.278-1.976-.501v-1.28l-2.504.195-.776.417h-.994l-.487-.049-1.207.67v1.261l-2.467 1.78.205.76h.5l-.131.724-.352.129-.019 1.892 2.132 2.428h.928l.056-.148h1.668l.481-.445h.946l.519.52 1.41.146-.187 1.875 1.565 2.763-.824 1.575.056.742.649.647v1.784l.852 1.146v1.482h.736c-4.096 5.029-10.33 8.25-17.305 8.25C12.009 46.625 2 36.615 2 24.312c0-3.097.636-6.049 1.781-8.732v-.696l.798-.969c.277-.523.574-1.033.891-1.53l.036.405-.926 1.125a22.14 22.14 0 00-.798 1.665v1.27l.927.446v1.765l.889 1.517.723.111.093-.52-.853-1.316-.167-1.279h.5l.211 1.316 1.233 1.799-.318.581.784 1.199 1.947.482v-.315l.779.111-.074.556.612.112.945.258 1.335 1.521 1.705.129.167 1.391-1.167.816-.055 1.242-.167.76 1.688 2.113.129.724s.612.166.687.166c.074 0 1.372.983 1.372.983v3.819l.463.13-.315 1.762.779 1.039-.144 1.746 1.029 1.809 1.321 1.154 1.328.024.13-.427-.976-.822.056-.408.175-.5.037-.51-.66-.02-.333-.418.548-.527.074-.398-.612-.175.036-.37.872-.132 1.326-.637.445-.816 1.391-1.78-.316-1.392.427-.741 1.279.039.861-.682.278-2.686.955-1.213.167-.779-.871-.279-.575-.943-1.965-.02-1.558-.594-.074-1.111-.52-.909-1.409-.021-.814-1.278-.723-.353-.037.39-1.316.078-.482-.671-1.373-.279-1.131 1.307-1.78-.302-.129-2.006-1.299-.222.521-.984-.149-.565-1.707 1.141-1.074-.131-.383-.839.234-.865.592-1.091 1.363-.69 2.632-.001-.007.803.946.44-.075-1.372.682-.686 1.376-.904.094-.636 1.372-1.428 1.459-.808-.129-.106.988-.93.362.096.166.208.375-.416.092-.041-.411-.058-.417-.139v-.4l.221-.181h.487l.223.098.193.39.236-.036v-.034l.068.023.684-.105.097-.334.39.098v.362l-.362.249h.001l.053.397 1.239.382.003.015.285-.024.019-.537-.982-.447-.056-.258.815-.278.036-.78-.852-.519-.056-1.315-1.168.574h-.426l.112-1.001-1.59-.375-.658.497v1.516l-1.183.375-.474.988-.514.083v-1.264l-1.112-.154-.556-.362-.224-.819 1.989-1.164.973-.296.098.654.542-.028.042-.329.567-.081.01-.115-.244-.101-.056-.348.697-.059.421-.438.023-.032.005.002.128-.132 1.465-.185.648.55-1.699.905 2.162.51.28-.723h.945l.334-.63-.668-.167v-.797l-2.095-.928-1.446.167-.816.427.056 1.038-.853-.13-.131-.574.817-.742-1.483-.074-.426.129-.185.5.556.094-.111.556-.945.056-.148.37-1.371.038s-.038-.778-.093-.778l1.075-.019.817-.798-.446-.223-.593.576-.984-.056-.593-.816h-1.261l-1.316.983h1.206l.11.353-.313.291 1.335.037.204.482-1.503-.056-.073-.371-.945-.204-.501-.278-1.125.009A22.188 22.188 0 0124.312 2c5.642 0 10.797 2.109 14.73 5.574l-.265.474-1.029.403-.434.471.1.549.531.074.32.8.916-.369.151 1.07h-.276l-.752-.111-.834.14-.807 1.14-1.154.181-.167.988.487.115-.141.635-1.146-.23-1.051.23-.223.585.182 1.228.617.289 1.035-.006.699-.063.213-.556 1.092-1.419.719.147.708-.64.132.5 1.742 1.175-.213.286-.785-.042.302.428.483.106.566-.236-.012-.682.251-.126-.202-.214-1.162-.648-.306-.861h.966l.309.306.832.717.035.867.862.918.321-1.258.597-.326.112 1.029.583.64 1.163-.02c.225.579.427 1.168.604 1.769l-.121.112zm-32.331-7.093l.584-.278.528.126-.182.709-.57.181-.36-.738zm3.099 1.669v.459h-1.334l-.5-.139.125-.32.641-.265h.876v.265h.192zm.614.64v.445l-.334.215-.416.077v-.737h.75zm-.376-.181v-.529l.459.418-.459.111zm.209 1.07v.433l-.319.32h-.709l.111-.486.335-.029.069-.167.513-.071zm-1.766-.889h.737l-.945 1.321-.39-.209.084-.556.514-.556zm3.018.737v.432h-.709l-.194-.28v-.402h.056l.847.25zm-.655-.594l.202-.212.341.212-.273.225-.27-.225zm28.55 5.767l.07-.082c.029.126.06.252.088.38l-.158-.298z"></path>
      <path d="M3.782 14.884v.696c.243-.568.511-1.122.798-1.665l-.798.969z"></path>
    </svg>
  );
}

export default WorldIcon;
```

**components/map-loading-holder.tsx**

```tsx
import WorldIcon from "../components/world-icon";

function MapLoadingHolder() {
  return (
    <div className="loading-holder">
      <WorldIcon className="icon" />
      <h1>Initializing the map</h1>
      <div className="icon-attribute">
        Icons made by{" "}
        <a href="https://www.freepik.com" title="Freepik">
          Freepik
        </a>{" "}
        from{" "}
        <a href="https://www.flaticon.com/" title="Flaticon">
          www.flaticon.com
        </a>
      </div>
    </div>
  );
}

export default MapLoadingHolder;
```

Now, putting everything together, put the application in an `.app-container` element, inside which there will be an absolutely positioned map element placed in a `map-wrapper` and a `MapLoadingHolder` component

Let's also add the `<Head> ... </Head>` component, you can specify meta tags and `title` for the site with it

Let's make the changes to the styles, add a nice background for the `.loading-holder`, also align its content in the center, add a pulsing animation for the icon, since the background is semi-transparent, add a colored shadow `text-shadow: 0px 0px 10px rgba (152, 207, 195 , 0.7);` to the element `<h1>Initializing the map</h1>`

Now when we open the map we will see a nice loading screen
![map-loading-screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vwagqi2munrggzfasy0t.gif)
 

Links to source code and running application

[dqunbp/using-mapbox-gl-with-react](https://github.com/dqunbp/using-mapbox-gl-with-react)

[Using mapbox-gl with React and Next.js](https://using-mapbox-gl-with-react.vercel.app?cache=3)

## Storing the map instance outside of `React`

I will explain how to store and use the `mapbox-gl` instance outside of `React` in my next article.