If you are creating a Mapbox GL map using React, there may come a time when you want to be able to trigger Redux or other actions from within said popup. If you’re not using react-map-gl, this article is for you. If you’re looking for spoilers, see this codepen for a full implementation.
What sets this tutorial apart from others is that they use setHTML or they create a second React app inside the first one, which prevents you from using your usual set of tools for managing state from in the popup. This tutorial doesn’t. It allows you to manage and control state inside a Mapbox GL popup.
To make this happen, we need the following things:
- A component that holds the popup content that holds a reference
- A declaration of the component adjacent to the element that hosts the mapbox-gl map
- Something to put into the popup, and
- A trigger for the popup to occur
Step 1: Create the Popup Surround
The first step is to create a component called <PopupBase />
. This is what’s going to surround the popup content.
import React, { useContext, useEffect, useRef } from "react";
import { mapContext } from "../context/mapContext";
import mapboxgl from "mapbox-gl";
const PopupBase = ({ children, lngLat, ...mapboxPopupProps }) => {
const { map } = useContext(mapContext);
const popupRef = useRef();
useEffect(() => {
const popup = new mapboxgl.Popup({})
.setLngLat(lngLat)
.setDOMContent(popupRef.current)
.addTo(map);
return popup.remove;
}, [children, lngLat]);
return (
<div style={{ display: "none" }}>
<div ref={popupRef}>{children}</div>
</div>
);
};
export default Popup;
Notice that we’re using “setDOMContent”. You want to use this method over the usual “setHTML”. With this, we’re going to place this by the map element.
Step 2: Put the Popup Surround Adjacent to the Map
Here, I use an active state value which holds a lnglat coordinate as a proxy for “should the popup be visible”. There are many ways to do this, and this may not be the best. But it works.
// Map.js - Mapbox GL Component
const [content, setContent] = useState(null);
const [popupLngLat, setPopupLngLat] = useState(null);
// ... (rest of the React component)
return (
<>
{popupLngLat && <PopupBase lngLat={popupLngLat}>{content}</PopupBase>}
<div ref={(el) => (mapContainer.current = el)} style={styles} />
</>
);
When the popupLngLat is set the first time, the PopupBase-component is triggered. In this example, we don’t have a function which removes the popupLngLat value when the popup closes. This still works, but it may make more sense to have actions that properly scrubs the state on close.
Steps 3,4: Add the Content in the Trigger
For this part, we have a component called PopupContent. In the codepen example, it’s just a set of react elements that have some arbitrary action. It can be whatever you choose it to be. Note that I build this content based on the array of features that is captured by the click event. Here, I set the content state hook to the content, as well as the lnglat of the popup. As shown before is sufficient to trigger the popup in the map.
map.on("click", "geojson-layer", (e) => {
const labels = e.features.map((feature) => (
<PopupContent
key={feature.properties.label}
label={feature.properties.label}
/>
));
setContent(labels);
setPopupLngLat(e.lngLat);
});
That’s all there is to it. At this point, you will be able to do anything in a Mapbox popup you could do elsewhere in your React app.
Full Implementation
You can see a working example of this idea on Code Sandbox:
In here, I use context instead of redux to propagate change to the footer of the application based on actions a user takes in the popup. But it can be redux. It can be anything you decide is worth doing.
Talk to us
If you are having problems with mapping in JavaScript, or if you want to make your maps more engaging, Let us know. Sparkgeo spends a fair amount of time thinking about these problems, and can be of service. If you’re having trouble in the steps before making your maps (especially in the data-wrangling aspect), Sparkgeo also has your back. Let us help you make sense of this mirror world.