← Files MapboxARCHIVED FILE
skills/mapbox-web-integration-patterns/AGENTS.md
7.09 KB · Sep 30, 2026 · 23:11 UTC
# 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:**
```javascript
// ✅ 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
```javascript
// ❌ 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:**
```javascript
map.on('load', () => {
// ✅ Now safe to add sources/layers
map.addSource('data', {...});
map.addLayer({...});
});
```
## Framework-Specific Patterns
### React
```javascript
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)
```javascript
'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)
```javascript
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
```javascript
<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
```typescript
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
```javascript
// ✅ 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
```javascript
// ✅ 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
```javascript
// ❌ 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
```javascript
// ❌ 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
```javascript
// ❌ 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
```javascript
// ❌ 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
```javascript
// Mark component as client-only
'use client';
// OR use dynamic import
const Map = dynamic(() => import('./Map'), { ssr: false });
```
### Next.js Pages Router
```javascript
// Disable SSR for map component
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('./Map'), { ssr: false });
```
### Check for Browser Environment
```javascript
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
```javascript
// ✅ 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)
SHA-256: 79e0dd6a0608578b225ea2175ab9f700b882ba563c46124265324ba361c363df