React + Mapbox GL JS: Custom Markers, Popups, and Bounds-Based Data Fetching
React + Mapbox GL JS: Custom Markers, Popups, and Bounds-Based Data Fetching If you've added a Mapbox map in a React app before, you've probably hit a wall pretty quickly: Mapbox GL JS manages its own DOM, and React manages a virtual DOM, and getting the two to play nicely takes some thought. Markers and popups are a great example of this tension — they're imperative APIs in a world where you want declarative components. This post walks through a pattern I've landed on for wrapping mapboxgl.Marker and mapboxgl.Popup in composable React components, and combining them with bounds-based data fetching to build something like a real estate or earthquake explorer map — where the data on the map updates as you pan and zoom. Here's what we'll cover: Wrapping mapboxgl.Marker in a React component that handles its own lifecycle Using createPortal to render custom marker content in React Fetching data from an API using the map's current bounding box Tracking an active marker with React state Wrapping mapboxgl.Popup in a component If you want to see the finished product first, the source code is on GitHub . The Core Challenge: Two DOMs Before we get into the code, it's worth understanding why this requires a pattern at all. When you use React's normal component tree, React controls the DOM. But mapboxgl.Marker also creates and manages DOM nodes — it places an element on the map at a specific geographic coordinate, handles repositioning as you pan, and removes itself cleanly when you're done with it. The approach here is to use React to manage the lifecycle of each marker (mount when data arrives, unmount when data goes away), while letting Mapbox handle the positioning . We use React's createPortal to render JSX content into a DOM node that we hand off to Mapbox. The example discussed below is a map that fetches earthquake data for the current viewport, displaying a custom Marker and Popup for each. As you move the map and the bounds change, new data is fetched and the Markers a