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 once2. 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 separately3. 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
- Code Splitting: Dynamic import mapbox-gl (large bundle)
- Lazy Loading: Load map component on demand
- Memoization: Use useMemo/React.memo for expensive computations
- 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)