DEV Community

Cover image for Adding a Custom Popup to a Map Layer Using React
Ben Tyler
Ben Tyler

Posted on • Edited on • Originally published at lostcreekdesigns.co

Adding a Custom Popup to a Map Layer Using React

This post is part of my Building Interactive Maps with React course - a course for anyone wanting to learn how to build interactive maps and integrate them into their React applications. If you enjoy this guide, then chances are you will enjoy the course too!

The previous posts in the series have covered

  • how to use Mapbox Studio to manage spatial data and create custom base maps (read post)
  • how to create a basic application using Mapbox and React (read post).

These posts are helpful in understanding the basics of Mapbox Studio and the relation between Studio and Mapbox GL JS. Understanding these fundamentals is essential when you start developing much larger, data-driven mapping applications.

The aim of this post is to provide an introduction to adding a variety of spatial data formats to a React application using Mapbox GL JS. Understanding how to add sources and layers to a map will open a lot of doors for the types of applications you can build using Mapbox GL JS.

If you do not care too much about the explanations and are just looking for a snippet, check out the Code Sandbox for this guide or scroll to the bottom of the post.

Deciding Between a Custom Style and Custom Code

I covered how to manage spatial data in Mapbox Studio using Datasets and Tilesets as well as how to add custom layers to a Mapbox style in earlier posts. If your spatial data is static and will not need to respond much to user inputs in your application, adding the spatial data to a custom Mapbox Style and then using that style in your application is probably the most ideal workflow.

However, if your application and map are fairly data-driven, this guide should be very relevant to your workflow. Here are a few common examples of when it is probably easier to bring spatial data into your map and application using Mapbox GL JS versus a custom style in Mapbox Studio. There are ways to accommodate these use cases using Mapbox Studio, but I just find it easier to manage them entirely using Mapbox GL JS.

  • the map needs to display data that updates frequently
    • i.e. a delivery map that shows near real-time status and position of drivers
  • the map needs to use data from a third party API
  • the map needs the ability to style and filter layer features based on user input

What We Will Build

Completed Interactive Map

We will use Mapbox GL JS and React to build an interactive map with several custom sources and layers. We will be adding sources and layers for

  • avalanche slide paths
  • nearby weather stations
  • bus routes
  • 3D terrain
  • the sky

The next sections will provide an overview of Sources and Layers followed by some concrete usage examples.

If you do not care too much about the explanations and are just looking for a snippet, check out the Code Sandbox for this guide.

Sources

I like to think of a Source as a mini datastore for my map. It tells Mapbox where to find my data as well as how to represent it. There are multiple types of Sources that you can use including: vector, raster, raster-dem, geojson, image, and video. This provides a lot of flexibility in terms of what kind of data can be added to a Mapbox GL JS application.

Each source type has their own configuration options, but you can generally do things like set the min and max zoom thresholds for a source. The Mapbox Style Specification provides a comprehensive summary of each type. For this guide though, we will be focused on the vector and geojson source types.

Adding a Vector Source

https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#vector

Probably the most common way of adding spatial data to a map is adding a vector tile source hosted by Mapbox. Sidenote: If you are not overly familiar with the difference between vector and raster data in the context of GIS, check out this helpful guide from Carto.

You can add one of Mapbox's tilesets or add your own custom tileset that is hosted on Mapbox. See this earlier post for instructions on how to create your own tileset.

// adding a Mapbox tileset
// method expects you to provide an id for the source
// as well some configuration options
map.addSource("mapbox-streets", {
  type: "vector",
  url: "mapbox://mapbox.mapbox-streets-v8",
})

// adding your own tileset
map.addSource("avalanche-paths", {
  type: "vector",
  url: "mapbox://lcdesigns.arckuvnm",
})
Enter fullscreen mode Exit fullscreen mode

Adding a GeoJSON Source

https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson

This method is great for adding in spatial data from third party APIs or pulling in data from your own APIs. You can define the GeoJSON inline, read GeoJSON directly from a local file, or hit an API endpoint that returns GeoJSON.

// inline geojson
// method expects you to provide an id for the source
// as well some configuration options
map.addSource("mapbox-streets", {
  type: "geojson",
    data: {
        "type": "Feature",
        "geometry": {
        "type": "Polygon",
        "coordinates": [
            [
                [-67.13734351262877, 45.137451890638886],
                [-66.96466, 44.8097],
                [-68.03252, 44.3252],
                [-69.06, 43.98],
                [-70.11617, 43.68405],
                [-70.64573401557249, 43.090083319667144],
                [-70.75102474636725, 43.08003225358635],
                [-70.79761105007827, 43.21973948828747],
                [-70.98176001655037, 43.36789581966826],
                [-70.94416541205806, 43.46633942318431],
                [-71.08482, 45.3052400000002],
                [-70.6600225491012, 45.46022288673396],
                [-70.30495378282376, 45.914794623389355],
                [-70.00014034695016, 46.69317088478567],
                [-69.23708614772835, 47.44777598732787],
                [-68.90478084987546, 47.184794623394396],
                [-68.23430497910454, 47.35462921812177],
                [-67.79035274928509, 47.066248887716995],
                [-67.79141211614706, 45.702585354182816],
                [-67.13734351262877, 45.137451890638886]
            ]
        ]
    }
});

// adding GeoJSON read from a file
import ExampleData from "./ExampleData.json";
map.addSource("avalanche-paths", {
  type: "geojson",
  data: ExampleData,
});

// adding GeoJSON from an API
import ExampleData from "./ExampleData.json";
map.addSource("avalanche-paths", {
  type: "geojson",
  data: "https://opendata.arcgis.com/datasets/4347f3565fbe4d5dbb97b016768b8907_0.geojson",
});
Enter fullscreen mode Exit fullscreen mode

Layers

Layers are the visual representation of a source's data, they are what actually get rendered on the map. Once you add a source to a map, you can create any number of layers using it. For instance, if I added a source that contained city parks, I could create the following three layers from that single source.

  • a fill layer that represents the park boundaries as shaded polygons
  • a line layer that represents the boundaries as an outline
  • a symbol layer that displays the park names as text labels

Mapbox supports a lot of different layer types including background, fill, line, symbol, raster, circle, fill-extrusion, heatmap, hillshade, and sky. It is beyond the scope of this guide to cover all of these layer types, but this guide will focus on the what you will be most likely to use, fill, line, symbol,and circle.

A note about layers is that they are rendered in the order they are defined. So if you want a layer to be below another one of your layers, make sure you add it first. Alternatively though, you can tell Mapbox which layer a layer should be added before/after. See this guide to learn how to do so.

Each layer is created in a similar fashion, but has its own unique set of layout and paint properties (aka how it looks) that can be configured. It is unfortunately beyond the scope of this guide to cover all of these configuration options, but the Mapbox docs do a great job. For a deeper dive into layers, check out the Mapbox Style Specification.

Adding a Fill Layer

https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#fill

Fill layers will be your go to for visualizing polygons on a map. Think use cases like boundaries, census tracts, bodies of water, avalanche paths, building footprints, etc. The general syntax for adding a layer is more or less the same regardless of layer type. The major differences between layer types is in the layout and paint configuration options (i.e how the layer is presented and styled).

// add a fill layer to the map
map.addLayer({
  id: "avalanche-paths-fill",
  type: "fill",
  source: "avalanche-paths",
  "source-layer": "Utah_Avalanche_Paths-9s9ups",
  paint: {
    "fill-opacity": 0.5,
    "fill-color": "#f05c5c",
  },
})
Enter fullscreen mode Exit fullscreen mode

Adding a Circle Layer

https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#circle

Circle layers are useful any time you want to visualize point data. A symbol layer can also be used to visualize point data but the simplicity of the circle layer type can be nice, especially if you want do things like data-driven styling.

// add a circle layer to the map
map.addLayer({
  id: "snotel-sites-circle",
  type: "circle",
  source: "snotel-sites",
  paint: {
    "circle-color": "#ffff00",
    "circle-radius": 8,
    "circle-stroke-color": "#333333",
    "circle-stroke-width": 2,
  },
})
Enter fullscreen mode Exit fullscreen mode

Adding a Line Layer

https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#line

Line layers are your best friend anytime you want to visualize a line string, think use cases like bus routes, Lyft routes, hiking tracks, rivers and streams, etc.

// add a line layer
map.addLayer({
  id: "bus-routes-line",
  type: "line",
  source: "bus-routes",
  paint: {
    "line-color": "#15cc09",
    "line-width": 4,
  },
})
Enter fullscreen mode Exit fullscreen mode

Adding a Symbol Layer

Symbol layers are the ones that took me the longest to get my head around. There are two primary use cases for symbol layers: 1) if you want to visualize data using an icon and 2) if you want to label map features with some text.

You can see all of the icons that are available for use as a symbol layer over at this page. Just hover over any of the icons to see the name (i.e. airfield-15). You can also create and upload your own icons, but that will likely be the topic of another post.

Adding a label layer is relatively straightforward too and you can use any of the properties (fields) in your data source as labels. In the example below, there is a field called "Station Name" that I am using to label features. I am using a Mapbox Expression (["get", "Station Name"]) to grab the values from the Station Name field.

// add a symbol layer - icon
map.addLayer({
  id: "bus-stops-symbol",
  type: "symbol",
  source: "bus-stops",
    layout: {
        icon-image: 'bus-15',
     }
});

// add a symbol layer - text label
map.addLayer({
  id: "snotel-sites-label",
  type: "symbol",
  source: "snotel-sites",
  layout: {
    "text-field": ["get", "Station Name"],
    "text-size": 14,
    "text-offset": [0, -1.5],
  },
  paint: {
    "text-color": "#ffff00",
    "text-halo-color": "#333333",
    "text-halo-width": 1,
  },
});
Enter fullscreen mode Exit fullscreen mode

Adding Sources and Layers to a React Map

With all of that foundation established (a lot of it!), the following steps should hopefully make a bit more sense. In this section, we are going to use these specific methods from Mapbox GL JS to add sources and layers to an interactive map in a React application.

Process Overview

Regardless of what type of spatial data you are adding to your application, there will always be two key components:

  • Adding a source
  • Adding a layer

Adding the source tells Mapbox that "hey, this is a data store that contains or more layers that could get added to the map". When you add a layer to a map, you then point it at the source and tell Mapbox how to represent the source on the map.

If you want to follow along outside of this post, you can check the Code Sandbox or the Github repo.

Process Implementation

The rest of the guide is going to pick up where my earlier Introduction to Mapbox and React post left off. I have put together a working snippet below filled with comments. I started out trying to explain every last bit of what was happening but think it is a lot more apparent in a lot of ways if I let the code speak for itself. I have provided links to the relevant Mapbox docs which do a much better job of explaining than I ever could. You can also refer to the primer above on sources and layers.

import React, { useRef, useEffect } from "react"
import mapboxgl from "mapbox-gl"
import SnotelSites from "./lcc_snotel_sites.json"
// import the mapbox styles
// alternatively can use a link tag in the head of public/index.html
// see https://docs.mapbox.com/mapbox-gl-js/api/
import "mapbox-gl/dist/mapbox-gl.css"
import "./app.css"

// Grab the access token from your Mapbox account
// I typically like to store sensitive things like this
// in a .env file
mapboxgl.accessToken = process.env.REACT_APP_MAPBOX_TOKEN

const App = () => {
  const mapContainer = useRef()

  // this is where all of our map logic is going to live
  // adding the empty dependency array ensures that the map
  // is only rendered once
  useEffect(() => {
    // create the map and configure it
    // check out the API reference for more options
    // https://docs.mapbox.com/mapbox-gl-js/api/map/
    const map = new mapboxgl.Map({
      container: mapContainer.current,
      style: "mapbox://styles/mapbox/outdoors-v11",
      center: [-111.75, 40.581],
      zoom: 12,
      pitch: 60,
      bearing: 80,
    })

    // only want to work with the map after it has fully loaded
    // if you try to add sources and layers before the map has loaded
    // things will not work properly
    map.on("load", () => {
      // add mapbox terrain dem source for 3d terrain rendering
      map.addSource("mapbox-dem", {
        type: "raster-dem",
        url: "mapbox://mapbox.mapbox-terrain-dem-v1",
        tileSize: 512,
        maxZoom: 16,
      })
      map.setTerrain({ source: "mapbox-dem" })

      // avalanche paths source
      // example of how to add a custom tileset hosted on Mapbox
      // you can grab the url from the details page for any tileset
      // you have created in Mapbox studio
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#vector
      map.addSource("avalanche-paths", {
        type: "vector",
        url: "mapbox://lcdesigns.arckuvnm",
      })

      // snotel sites source
      // example of using a geojson source
      // data is hosted locally as part of the application
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson
      map.addSource("snotel-sites", {
        type: "geojson",
        data: SnotelSites,
      })

      // bus routes source
      // another example of using a geojson source
      // this time we are hitting an ESRI API that returns
      // data in the geojson format
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson
      map.addSource("bus-routes", {
        type: "geojson",
        data:
          "https://opendata.arcgis.com/datasets/4347f3565fbe4d5dbb97b016768b8907_0.geojson",
      })

      // avalanche paths - fill layer
      // source-layer can be grabbed from the tileset details page
      // in Mapbox studio
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#fill
      map.addLayer({
        id: "avalanche-paths-fill",
        type: "fill",
        source: "avalanche-paths",
        "source-layer": "Utah_Avalanche_Paths-9s9ups",
        paint: {
          "fill-opacity": 0.5,
          "fill-color": "#f05c5c",
        },
      })

      // snotel sites - circle layer
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#circle
      map.addLayer({
        id: "snotel-sites-circle",
        type: "circle",
        source: "snotel-sites",
        paint: {
          "circle-color": "#1d1485",
          "circle-radius": 8,
          "circle-stroke-color": "#ffffff",
          "circle-stroke-width": 2,
        },
      })

      // snotel sites - label layer
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#symbol
      map.addLayer({
        id: "snotel-sites-label",
        type: "symbol",
        source: "snotel-sites",
        layout: {
          "text-field": ["get", "Station Name"],
          "text-size": 16,
          "text-offset": [0, -1.5],
        },
        paint: {
          "text-color": "#1d1485",
          "text-halo-color": "#ffffff",
          "text-halo-width": 0.5,
        },
      })

      // bus routes - line layer
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#line
      map.addLayer({
        id: "bus-routes-line",
        type: "line",
        source: "bus-routes",
        paint: {
          "line-color": "#15cc09",
          "line-width": 4,
        },
      })

      // add a sky layer
      // the sky layer is a custom mapbox layer type
      // see https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#sky
      map.addLayer({
        id: "sky",
        type: "sky",
        paint: {
          "sky-type": "atmosphere",
          "sky-atmosphere-sun": [0.0, 90.0],
          "sky-atmosphere-sun-intensity": 15,
        },
      })
    })

    // cleanup function to remove map on unmount
    return () => map.remove()
  }, [])

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

export default App
Enter fullscreen mode Exit fullscreen mode

Next Steps

This guide just scratches the surface in terms of the types of sources and layers that can be added to a map using Mapbox GL JS. I encourage you to explore the Mapbox docs and extend my examples. You could try things like...

  • tweaking and expanding the layer styling
  • adding your own sources and layers

If you found thus post useful, give me a follow on Twitter or consider picking up a copy of the Building Interactive Maps with React course.

Useful Links and Resources

Top comments (0)