All skills
mapbox avatar

/mapbox-web-integration-patterns

@f5ae7de official
by mapboxmapbox/mapbox-agent-skills80 stars
17

Official integration patterns for Mapbox GL JS across popular web frameworks (React, Vue, Svelte, Angular). Covers setup, lifecycle management, token handling, search integration, and common pitfalls. Based on Mapbox's create-web-app scaffolding tool.

Use this Skill: https://skilld.dev/gh/mapbox/mapbox-agent-skills/mapbox-web-integration-patterns

This session only. Nothing lands on disk.

AGENTS.md

≈1.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Mapbox Framework Integration Guide

Quick reference for integrating Mapbox GL JS with React, Vue, Svelte, Angular, and Next.js.

Critical Integration Rules

1. Map Lifecycle Management

Must properly initialize and cleanup in all frameworks:

// ✅ Initialize once, cleanup on unmount
useEffect(() => {
  const map = new mapboxgl.Map({...});

  return () => map.remove(); // Critical: prevents memory leaks
}, []); // Empty deps = mount once

2. Never Re-initialize Map

Problem: Creating new map instances causes memory leaks Solution: Initialize once with empty dependency array

// ❌ Bad: Re-creates map on every state change
useEffect(() => {
  const map = new mapboxgl.Map({...});
}, [someState]); // Re-runs on state change!

// ✅ Good: Create once, update separately
useEffect(() => {
  const map = new mapboxgl.Map({...});
  return () => map.remove();
}, []); // Only runs once

useEffect(() => {
  if (map) map.setCenter(center);
}, [center]); // Update separately

3. Wait for Map Load

All map operations must wait for 'load' event:

map.on('load', () => {
  // ✅ Now safe to add sources/layers
  map.addSource('data', {...});
  map.addLayer({...});
});

Framework-Specific Patterns

React

import { useEffect, useRef } from 'react';

function MapComponent() {
  const mapContainer = useRef(null);
  const map = useRef(null);

  useEffect(() => {
    if (map.current) return; // Initialize only once

    map.current = new mapboxgl.Map({
      container: mapContainer.current,
      style: 'mapbox://styles/mapbox/streets-v12',
      center: [-122.4, 37.8],
      zoom: 12
    });

    return () => map.current.remove();
  }, []);

  // Update map when props change
  useEffect(() => {
    if (!map.current) return;
    map.current.setCenter([lng, lat]);
  }, [lng, lat]);

  return <div ref={mapContainer} style={{ width: '100%', height: '400px' }} />;
}

Next.js (with SSR)

'use client'; // Mark as client component

import dynamic from 'next/dynamic';

// Option 1: Dynamic import (recommended)
const Map = dynamic(() => import('./Map'), { ssr: false });

// Option 2: Check for window
useEffect(() => {
  if (typeof window === 'undefined') return;
  const mapboxgl = require('mapbox-gl');
  // Initialize map...
}, []);

Vue 3 (Composition API)

import { onMounted, onUnmounted, ref } from 'vue';

export default {
  setup() {
    const mapContainer = ref(null);
    let map = null;

    onMounted(() => {
      map = new mapboxgl.Map({
        container: mapContainer.value,
        style: 'mapbox://styles/mapbox/streets-v12'
      });
    });

    onUnmounted(() => {
      map?.remove();
    });

    return { mapContainer };
  }
};

Svelte

<script>
  import { onMount, onDestroy } from 'svelte';
  import mapboxgl from 'mapbox-gl';

  let mapContainer;
  let map;

  onMount(() => {
    map = new mapboxgl.Map({
      container: mapContainer,
      style: 'mapbox://styles/mapbox/streets-v12'
    });
  });

  onDestroy(() => {
    map?.remove();
  });
</script>

<div bind:this={mapContainer}></div>

Angular

import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import mapboxgl from 'mapbox-gl';

@Component({
  selector: 'app-map',
  template: '<div #mapContainer></div>'
})
export class MapComponent implements OnInit, OnDestroy {
  @ViewChild('mapContainer', { static: true }) mapContainer!: ElementRef;
  private map!: mapboxgl.Map;

  ngOnInit() {
    this.map = new mapboxgl.Map({
      container: this.mapContainer.nativeElement,
      style: 'mapbox://styles/mapbox/streets-v12'
    });
  }

  ngOnDestroy() {
    this.map?.remove();
  }
}

State Management Patterns

Updating Map from State

// ✅ Separate effects for different updates
useEffect(() => {
  if (!map.current) return;
  map.current.setCenter(center);
}, [center]);

useEffect(() => {
  if (!map.current) return;
  map.current.setZoom(zoom);
}, [zoom]);

useEffect(() => {
  if (!map.current) return;
  map.current.getSource('data')?.setData(geojson);
}, [geojson]);

Extracting State from Map

// ✅ Update component state from map events
useEffect(() => {
  if (!map.current) return;

  const handleMove = () => {
    setCenter(map.current.getCenter());
    setZoom(map.current.getZoom());
  };

  map.current.on('move', handleMove);
  return () => map.current.off('move', handleMove);
}, []);

Common Issues

Issue: Map Not Visible

// ❌ Container has no height
<div ref={mapContainer}></div>

// ✅ Container must have explicit dimensions
<div ref={mapContainer} style={{ width: '100%', height: '400px' }}></div>

Issue: Map Renders Before Container

// ❌ Map created before DOM ready
const map = new mapboxgl.Map({...}); // Too early!

// ✅ Wait for mount
useEffect(() => {
  const map = new mapboxgl.Map({...}); // DOM is ready
}, []);

Issue: Memory Leaks in SPA

// ❌ No cleanup
useEffect(() => {
  const map = new mapboxgl.Map({...});
  // Missing return statement
}, []);

// ✅ Always cleanup
useEffect(() => {
  const map = new mapboxgl.Map({...});
  return () => map.remove(); // Critical!
}, []);

Issue: Multiple Map Instances on Same Container

// ❌ Multiple initializations — creates overlapping maps, leaks memory
useEffect(() => {
  const map = new mapboxgl.Map({...});
}, [someState]); // Runs multiple times!

// ✅ Initialize once, check if exists
useEffect(() => {
  if (map.current) return; // Don't re-initialize
  map.current = new mapboxgl.Map({...});
}, []);

SSR/Hydration Considerations

Next.js App Router

// Mark component as client-only
'use client';

// OR use dynamic import
const Map = dynamic(() => import('./Map'), { ssr: false });

Next.js Pages Router

// Disable SSR for map component
import dynamic from 'next/dynamic';

const Map = dynamic(() => import('./Map'), { ssr: false });

Check for Browser Environment

useEffect(() => {
  // Only runs in browser
  if (typeof window === 'undefined') return;

  const map = new mapboxgl.Map({...});
  return () => map.remove();
}, []);

Performance Tips

  1. Code Splitting: Dynamic import mapbox-gl (large bundle)
  2. Lazy Loading: Load map component on demand
  3. Memoization: Use useMemo/React.memo for expensive computations
  4. Debounce Updates: Don't update map on every keystroke
// ✅ Debounce state updates
const debouncedSearch = useMemo(() => debounce((query) => updateMap(query), 300), []);

Integration Checklist

✅ Map initialized in mount hook (useEffect, onMounted, etc.) ✅ Cleanup with map.remove() in unmount hook ✅ Empty dependency array (initialize once) ✅ Container has explicit width/height ✅ Operations wait for 'load' event ✅ SSR handled (disable or check for window) ✅ State updates in separate effects ✅ Event listeners removed on cleanup ✅ No re-initialization on state changes ✅ Map reference stored (useRef, let, class property)

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill contains safe, standard documentation, integration patterns, and best practices for Mapbox GL JS across various web frameworks.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    1/2 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at f5ae7de. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 9 hours ago.

Activeupdated 2 months ago

README badge

README badge for mapbox/mapbox-agent-skills/mapbox-web-integration-patterns

Provides official integration patterns for Mapbox GL JS across React, Vue, Svelte, Angular, and vanilla JavaScript, with lifecycle management, token handling, and Search JS integration. Covers setup in each framework, cleanup to prevent memory leaks, and common mistakes like storing map instances in Vue's data() or forgetting map.remove() calls.

Generated from the current SKILL.md.

Does this skill cover all major web frameworks?
Yes. It covers React, Vue, Svelte, Angular, Next.js, vanilla JavaScript, and Web Components. Framework-specific patterns are documented in reference files (e.g., references/vue.md, references/angular.md).
What version of Mapbox GL JS should I use?
v3.x is recommended (minimum v3.0.0). v2.x is legacy and no longer actively developed. v3.x requires WebGL 2 and has improved TypeScript types, but core initialization patterns work the same in both versions.
How do I handle the Mapbox access token securely?
Store it in environment variables (e.g., VITE_MAPBOX_ACCESS_TOKEN for Vite, REACT_APP_MAPBOX_TOKEN for Create React App). Set it via mapboxgl.accessToken or pass it per-map in the constructor. The skill has a dedicated reference file on token management per bundler.
What's the most common mistake when integrating Mapbox?
Forgetting to call map.remove() in cleanup functions. Without it, WebGL contexts and event listeners accumulate, causing memory leaks. The skill documents this and three other critical mistakes with examples.
Can I use Mapbox Search JS with these patterns?
Yes. The skill includes React + Search JS integration examples and covers SearchBox configuration, positioning, and the required packages (@mapbox/search-js-react for React, @mapbox/search-js-web for other frameworks).

Generated from the current SKILL.md. These answers refresh after source changes.