惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
Simon Willison's Weblog
Simon Willison's Weblog
B
Blog
V
Visual Studio Blog
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
S
SegmentFault 最新的问题
博客园 - 司徒正美
博客园 - 【当耐特】
T
Tenable Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
宝玉的分享
宝玉的分享
N
Netflix TechBlog - Medium
S
Secure Thoughts
Hugging Face - Blog
Hugging Face - Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
IT之家
IT之家
Google DeepMind News
Google DeepMind News
Last Week in AI
Last Week in AI
大猫的无限游戏
大猫的无限游戏
PCI Perspectives
PCI Perspectives
H
Hackread – Cybersecurity News, Data Breaches, AI and More
阮一峰的网络日志
阮一峰的网络日志
P
Privacy International News Feed
N
News and Events Feed by Topic
H
Hacker News: Front Page
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
F
Full Disclosure
Google Online Security Blog
Google Online Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Heimdal Security Blog
Project Zero
Project Zero
C
CERT Recently Published Vulnerability Notes
MyScale Blog
MyScale Blog
AI
AI
月光博客
月光博客
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
WordPress大学
WordPress大学
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
CXSECURITY Database RSS Feed - CXSecurity.com
Spread Privacy
Spread Privacy
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
SecWiki News
SecWiki News
C
Cisco Blogs
The Last Watchdog
The Last Watchdog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
React + Mapbox GL JS: Custom Markers, Popups, and Bounds-Based Data Fetching
Chris Whong · 2026-06-03 · via DEV Community

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 and Popups are updated accordingly. Try it out:


Setting Up: A Map Component That Fetches Data

Start with a basic Mapbox GL JS map in a React component. The setup here is pretty standard — a useRef for the map instance, a useRef for the container element, and a useEffect to initialize the map on mount and clean it up on unmount.

// src/Map.jsx
import React, { useEffect, useRef, useState, useCallback } from 'react'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

function Map() {
    // mapRef holds the mapboxgl.Map instance so we can call methods on it
    // without triggering re-renders when it changes
    const mapRef = useRef()

    // mapContainerRef points to the DOM element that Mapbox will render into
    const mapContainerRef = useRef()

    // earthquakeData holds the GeoJSON FeatureCollection returned by the API
    const [earthquakeData, setEarthquakeData] = useState()

    // useCallback prevents this function from being recreated on every render,
    // which matters because we pass it to map.on() event listeners
    const getBboxAndFetch = useCallback(async () => {
        // getBounds() returns the lat/lng bounds of what's currently visible on screen
        const bounds = mapRef.current.getBounds()
        try {
            const data = await fetch(
                // Pass the SW and NE corners of the viewport as query params
                // so the API only returns earthquakes within the current view
                `https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson` +
                `&starttime=2024-01-01&endtime=2024-01-30` +
                `&minlatitude=${bounds._sw.lat}&maxlatitude=${bounds._ne.lat}` +
                `&minlongitude=${bounds._sw.lng}&maxlongitude=${bounds._ne.lng}`
            ).then(d => d.json())
            setEarthquakeData(data)
        } catch (error) {
            console.error(error)
        }
    }, [])

    useEffect(() => {
        mapRef.current = new mapboxgl.Map({
            accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
            container: mapContainerRef.current,
            center: [124, -1.98],  // Start centered on Indonesia, an earthquake-prone region
            minZoom: 5.5,          // Prevent zooming out too far — keeps API responses manageable
            zoom: 5.5
        })

        // Fetch once when the map finishes its initial load
        mapRef.current.on('load', getBboxAndFetch)

        // Fetch again whenever the user pans or zooms to a new area
        mapRef.current.on('moveend', getBboxAndFetch)

        // Clean up the map instance when this component unmounts
        return () => mapRef.current.remove()
    }, [])

    return <div id='map-container' ref={mapContainerRef} />
}

Enter fullscreen mode Exit fullscreen mode

The key piece here is getBboxAndFetch. It reads the map's current bounds using Map.getBounds(), constructs a URL to fetch earthquake data with those bounds as query parameters, and stores the response in state. It fires once on load and again on every moveend — so every time the user pans or zooms, new data comes in for all earthquakes that occurred in whatever part of the world is in view.

I'm wrapping it in useCallback to avoid it being recreated on every render, which would cause issues with the event listener setup.


The Marker Component

Here's where things get interesting. Rather than imperatively adding and removing markers in the Map component, we can delegate that work to a dedicated Marker component that handles its own lifecycle.

The trick is using createPortal to render JSX content into a DOM node that we create ourselves and hand to mapboxgl.Marker as its element.

// src/Marker.jsx
import { useEffect, useRef } from "react"
import mapboxgl from "mapbox-gl"
import { createPortal } from "react-dom"

const Marker = ({ map, feature, isActive, onClick }) => {
    const { geometry, properties } = feature

    // markerRef holds the mapboxgl.Marker instance so we can remove it on unmount
    const markerRef = useRef(null)

    // contentRef is a plain DOM div that we create once — this is the bridge between
    // React's rendering and Mapbox's marker system. We hand it to Mapbox, and
    // React renders into it via createPortal below.
    const contentRef = useRef(document.createElement("div"))

    useEffect(() => {
        // Pass contentRef.current as the element for the marker.
        // Mapbox will position this div on the map at the given coordinates.
        markerRef.current = new mapboxgl.Marker(contentRef.current)
            .setLngLat([geometry.coordinates[0], geometry.coordinates[1]])
            .addTo(map)

        // Remove the marker from the map when this component unmounts.
        // This fires automatically when the parent re-renders with new data
        // and this earthquake feature is no longer in the result set.
        return () => markerRef.current.remove()
    }, [])

    // createPortal renders our JSX into contentRef.current — the same div
    // that Mapbox is positioning on the map. React controls the content,
    // Mapbox controls the placement.
    return createPortal(
        <div
            onClick={() => onClick(feature)}
            style={{
                display: "inline-block",
                padding: "2px 10px",
                borderRadius: "50px",
                // Flip colors when this marker is the active (clicked) one
                backgroundColor: isActive ? "#333" : "#fff",
                boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.5)",
                fontFamily: "Arial, sans-serif",
                fontSize: "14px",
                fontWeight: "bold",
                color: isActive ? "#fff" : "#333",
            }}
        >
            {/* Show the earthquake's magnitude as the marker label */}
            {properties.mag}
        </div>,
        contentRef.current
    )
}

export default Marker

Enter fullscreen mode Exit fullscreen mode

Let's break down what's happening:

  1. contentRef holds a plain DOM div that we create once and never touch directly
  2. We pass that div to new mapboxgl.Marker(contentRef.current) — Mapbox takes ownership of positioning it on the map
  3. createPortal renders our JSX into that same div, so React controls the content while Mapbox controls the placement
  4. The useEffect cleanup removes the marker from the map when the component unmounts

The result is that you can build marker content using normal React patterns — JSX, props, conditional styles, event handlers — and it all "just works" inside Mapbox's marker system.


Rendering Markers from Data

Back in the Map component, rendering markers is now straightforward. Map over the earthquake features, render a Marker for each one, and use feature.id as the React key. That last part is important — when new data comes in after a pan, React will diff the keys and only create new Marker instances for earthquakes that weren't in the previous result set.

// In Map.jsx

// activeFeature holds the full GeoJSON Feature of the currently selected marker,
// or undefined if no marker has been clicked yet
const [activeFeature, setActiveFeature] = useState()

// Called by a Marker when it's clicked — stores the feature so we can
// highlight the active marker and position the popup
const handleMarkerClick = (feature) => {
    setActiveFeature(feature)
}

return (
    <>
        {/* The div that Mapbox renders the map canvas into */}
        <div id='map-container' ref={mapContainerRef} />

        {/* Guard against mapRef.current being undefined on the first render,
            then render one Marker component per earthquake feature */}
        {mapRef.current && earthquakeData?.features?.map((feature) => (
            <Marker
                key={feature.id}        // Stable ID lets React reuse existing markers on re-renders
                map={mapRef.current}    // The map instance, so Marker can call .addTo(map)
                feature={feature}
                isActive={activeFeature?.id === feature.id}  // True only for the clicked marker
                onClick={handleMarkerClick}
            />
        ))}
    </>
)

Enter fullscreen mode Exit fullscreen mode

activeFeature stores the full feature object of the currently clicked marker. Passing isActive={activeFeature?.id === feature.id} to each Marker lets them style themselves differently when selected — in our case, flipping to a dark background and light text.


The Popup Component

Popups follow a similar pattern to markers. One difference is that we only ever need one popup instance — it just moves around based on which marker is active.

// src/Popup.jsx
import { useEffect, useRef } from "react"
import { createPortal } from "react-dom"
import mapboxgl from 'mapbox-gl'

const Popup = ({ map, activeFeature }) => {
    // popupRef holds the single mapboxgl.Popup instance for its whole lifetime
    const popupRef = useRef()

    // contentRef is the same portal trick as in Marker — a plain DOM div
    // that React renders into, which we then pass to the Mapbox popup
    const contentRef = useRef(document.createElement("div"))

    // Runs once on mount: create the popup instance but don't add it to the map yet.
    // We wait until activeFeature is set before positioning and showing it.
    useEffect(() => {
        if (!map) return
        popupRef.current = new mapboxgl.Popup({
            closeOnClick: false,  // Keep the popup open when the user clicks elsewhere on the map
            offset: 20            // Push the popup up slightly so it doesn't overlap the marker
        })
        // Remove the popup from the map when this component unmounts
        return () => popupRef.current.remove()
    }, [])

    // Runs whenever activeFeature changes: reposition the popup and update its content
    useEffect(() => {
        if (!activeFeature) return

        popupRef.current
            .setLngLat(activeFeature.geometry.coordinates)  // Move popup to the clicked marker's location
            .setHTML(contentRef.current.outerHTML)           // Serialize the React-rendered content to an HTML string
            .addTo(map)                                      // Add (or re-add) the popup to the map
    }, [activeFeature])

    // Render the popup content into contentRef via a portal.
    // By the time the second useEffect above reads contentRef.current.outerHTML,
    // React will have already rendered this JSX into the div.
    return createPortal(
        <div>
            <table>
                <tbody>
                    <tr>
                        <td><strong>Time</strong></td>
                        {/* Convert the Unix timestamp (ms) to a human-readable local time */}
                        <td>{new Date(activeFeature?.properties.time).toLocaleString()}</td>
                    </tr>
                    <tr>
                        <td><strong>Magnitude</strong></td>
                        <td>{activeFeature?.properties.mag}</td>
                    </tr>
                    <tr>
                        <td><strong>Place</strong></td>
                        <td>{activeFeature?.properties.place}</td>
                    </tr>
                </tbody>
            </table>
        </div>,
        contentRef.current
    )
}

export default Popup

Enter fullscreen mode Exit fullscreen mode

There are two useEffect hooks here with different jobs:

  • The first runs once on mount and creates the mapboxgl.Popup instance (but doesn't add it to the map yet)
  • The second runs whenever activeFeature changes, and repositions the popup + updates its content

One thing to note: setHTML takes the outerHTML of contentRef.current, not a reference to the element itself. This is because Mapbox's popup API works with HTML strings for its content. The createPortal has already rendered our JSX into that DOM node by the time this runs, so outerHTML gives us the rendered output as a string.


Putting It All Together

The final Map component renders the map container, maps over the earthquake data to produce Marker components, and includes a single Popup that tracks the active feature:

// In Map.jsx

return (
    <>
        {/* The map canvas container */}
        <div id='map-container' ref={mapContainerRef} />

        {/* One Marker per earthquake — guarded so we don't render before the map exists */}
        {mapRef.current && earthquakeData?.features?.map((feature) => (
            <Marker
                key={feature.id}
                map={mapRef.current}
                feature={feature}
                isActive={activeFeature?.id === feature.id}
                onClick={handleMarkerClick}
            />
        ))}

        {/* A single Popup instance that repositions itself when activeFeature changes */}
        {mapRef.current && (
            <Popup map={mapRef.current} activeFeature={activeFeature} />
        )}
    </>
)

Enter fullscreen mode Exit fullscreen mode

The mapRef.current && guards are there because the map instance doesn't exist until after the first render — we don't want to pass undefined as the map prop.


Where to Take It From Here

This pattern is intentionally minimal. Some things you'd want to add in a real app:

  • Loading state: Show a spinner or disable interaction while a fetch is in progress
  • Error handling: Surface API errors in the UI instead of just logging them
  • Clustering: Group nearby markers when zoomed out — Mapbox GL JS has good support for this if you switch to a GeoJSON source/layer approach
  • Your own data: Swap out the USGS API for anything that returns GeoJSON and can be filtered by bounding box. This pattern works well for store locators, property listings, event maps, etc.

The full source code for the earthquake demo is on GitHub if you want to run it locally or use it as a starting point.