React Map GL
Complete guide to using React Map GL for building interactive maps in Next.js applications with Mapbox GL JS and MapLibre GL JS.
Introduction to React Map GL
React Map GL is a suite of React components that provide a fully reactive wrapper for Mapbox GL JS and MapLibre GL JS. Created by Uber's Visualization team (part of vis.gl), it enables developers to build powerful, performant mapping applications using familiar React patterns and reactive programming principles.
Key Features
Fully Reactive Architecture
- Controlled components with React state management
- Declarative API for map configuration
- Seamless integration with React ecosystem
- Predictable state synchronization
Dual Library Support
- Works with both Mapbox GL JS and MapLibre GL JS
- Easy switching between libraries
- Consistent API across both implementations
- Future-proof architecture
High Performance
- WebGL-based rendering
- Efficient handling of large datasets
- Optimized for mobile devices
- Smooth animations and transitions
Rich Component Library
- Map container with full camera control
- Markers, popups, and navigation controls
- Source and layer management
- GeolocateControl and ScaleControl
Developer Experience
- TypeScript definitions included
- Comprehensive documentation
- Extensive example collection
- Active community support
Installation
Using Mapbox GL JS
yarn add react-map-gl mapbox-gl
For TypeScript projects:
yarn add react-map-gl mapbox-gl @types/mapbox-gl
Using MapLibre GL JS
yarn add react-map-gl maplibre-gl
Basic Setup
Mapbox GL JS Version
'use client';
import { useState } from 'react';
import Map from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
export default function MapComponent() {
const [viewState, setViewState] = useState({
longitude: -74.0060,
latitude: 40.7128,
zoom: 12
});
return (
<Map
{...viewState}
onMove={evt => setViewState(evt.viewState)}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
style={{ width: '100%', height: '600px' }}
mapStyle="mapbox://styles/mapbox/streets-v12"
/>
);
}
MapLibre GL JS Version
'use client';
import { useState } from 'react';
import Map from 'react-map-gl/maplibre';
import 'maplibre-gl/dist/maplibre-gl.css';
export default function MapComponent() {
const [viewState, setViewState] = useState({
longitude: -74.0060,
latitude: 40.7128,
zoom: 12
});
return (
<Map
{...viewState}
onMove={evt => setViewState(evt.viewState)}
style={{ width: '100%', height: '600px' }}
mapStyle="https://demotiles.maplibre.org/style.json"
/>
);
}
Core Concepts
State Management
React Map GL embraces React's controlled component pattern, where the map's state is managed by React:
Controlled Component (Recommended)
const [viewState, setViewState] = useState({
longitude: -122.4,
latitude: 37.8,
zoom: 14,
bearing: 0,
pitch: 0
});
<Map
{...viewState}
onMove={evt => setViewState(evt.viewState)}
/>
Uncontrolled Component
<Map
initialViewState={{
longitude: -122.4,
latitude: 37.8,
zoom: 14
}}
/>
Why Controlled Components?
- Synchronize map with other UI elements
- Implement custom navigation controls
- Persist and restore map state
- React to external state changes
Map Instance Access
Access the underlying Mapbox/MapLibre instance when needed:
import { useRef, useEffect } from 'react';
import { MapRef } from 'react-map-gl/mapbox';
export default function MapWithRef() {
const mapRef = useRef<MapRef>(null);
useEffect(() => {
if (mapRef.current) {
const map = mapRef.current.getMap();
// Access native Mapbox GL methods
map.on('load', () => {
console.log('Map loaded');
});
}
}, []);
return (
<Map
ref={mapRef}
initialViewState={{
longitude: -122.4,
latitude: 37.8,
zoom: 14
}}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
/>
);
}
Adding Markers
Basic Marker
'use client';
import Map, { Marker } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
export default function MapWithMarker() {
return (
<Map
initialViewState={{
longitude: -74.0060,
latitude: 40.7128,
zoom: 12
}}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
<Marker
longitude={-74.0060}
latitude={40.7128}
anchor="bottom"
>
<div style={{
fontSize: '30px',
cursor: 'pointer'
}}>
📍
</div>
</Marker>
</Map>
);
}
Multiple Markers from Data
'use client';
import { useState } from 'react';
import Map, { Marker, Popup } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
interface Location {
id: number;
name: string;
longitude: number;
latitude: number;
}
const locations: Location[] = [
{ id: 1, name: 'Empire State Building', longitude: -73.9857, latitude: 40.7484 },
{ id: 2, name: 'Central Park', longitude: -73.9654, latitude: 40.7829 },
{ id: 3, name: 'Times Square', longitude: -73.9855, latitude: 40.7580 },
];
export default function MapWithMarkers() {
const [popupInfo, setPopupInfo] = useState<Location | null>(null);
return (
<Map
initialViewState={{
longitude: -73.9857,
latitude: 40.7484,
zoom: 12
}}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
{locations.map(location => (
<Marker
key={location.id}
longitude={location.longitude}
latitude={location.latitude}
anchor="bottom"
onClick={e => {
e.originalEvent.stopPropagation();
setPopupInfo(location);
}}
>
<div style={{
fontSize: '24px',
cursor: 'pointer',
transform: 'translate(-50%, -100%)'
}}>
📍
</div>
</Marker>
))}
{popupInfo && (
<Popup
longitude={popupInfo.longitude}
latitude={popupInfo.latitude}
anchor="bottom"
onClose={() => setPopupInfo(null)}
>
<div>
<h3>{popupInfo.name}</h3>
<p>
Coordinates: {popupInfo.latitude.toFixed(4)}, {popupInfo.longitude.toFixed(4)}
</p>
</div>
</Popup>
)}
</Map>
);
}
Custom Marker Component
import { Marker } from 'react-map-gl/mapbox';
interface CustomMarkerProps {
longitude: number;
latitude: number;
type: 'restaurant' | 'hotel' | 'attraction';
onClick?: () => void;
}
const markerIcons = {
restaurant: '🍴',
hotel: '🏨',
attraction: '🎭',
};
const markerColors = {
restaurant: '#FF6B6B',
hotel: '#4ECDC4',
attraction: '#45B7D1',
};
export function CustomMarker({ longitude, latitude, type, onClick }: CustomMarkerProps) {
return (
<Marker
longitude={longitude}
latitude={latitude}
anchor="bottom"
onClick={onClick}
>
<div
style={{
backgroundColor: markerColors[type],
borderRadius: '50% 50% 50% 0',
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
cursor: 'pointer',
transform: 'rotate(-45deg)',
boxShadow: '0 2px 4px rgba(0,0,0,0.3)',
}}
>
<span style={{ transform: 'rotate(45deg)' }}>
{markerIcons[type]}
</span>
</div>
</Marker>
);
}
Navigation Controls
Built-in Controls
'use client';
import Map, {
NavigationControl,
GeolocateControl,
FullscreenControl,
ScaleControl
} from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
export default function MapWithControls() {
return (
<Map
initialViewState={{
longitude: -122.4,
latitude: 37.8,
zoom: 14
}}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
{/* Navigation control (zoom and compass) */}
<NavigationControl position="top-right" />
{/* Geolocate control (find user location) */}
<GeolocateControl
position="top-right"
trackUserLocation
showUserHeading
/>
{/* Fullscreen control */}
<FullscreenControl position="top-right" />
{/* Scale bar */}
<ScaleControl
position="bottom-left"
unit="metric"
/>
</Map>
);
}
Working with Layers
GeoJSON Layer
'use client';
import Map, { Source, Layer, LayerProps } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
const geojsonData = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [
[-122.48, 37.83],
[-122.47, 37.82],
[-122.46, 37.81]
]
},
properties: {
name: 'Sample Route'
}
}
]
};
const lineLayer: LayerProps = {
id: 'route',
type: 'line',
paint: {
'line-color': '#0080ff',
'line-width': 4
}
};
export default function MapWithGeoJSON() {
return (
<Map
initialViewState={{
longitude: -122.47,
latitude: 37.82,
zoom: 14
}}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
<Source id="route-source" type="geojson" data={geojsonData}>
<Layer {...lineLayer} />
</Source>
</Map>
);
}
Map Events
Click Events
'use client';
import { useState } from 'react';
import Map, { Marker, MapLayerMouseEvent } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
interface MarkerData {
longitude: number;
latitude: number;
id: number;
}
export default function ClickableMap() {
const [markers, setMarkers] = useState<MarkerData[]>([]);
const [nextId, setNextId] = useState(1);
const handleMapClick = (event: MapLayerMouseEvent) => {
const { lng, lat } = event.lngLat;
setMarkers(prev => [
...prev,
{
longitude: lng,
latitude: lat,
id: nextId
}
]);
setNextId(prev => prev + 1);
};
const removeMarker = (id: number) => {
setMarkers(prev => prev.filter(m => m.id !== id));
};
return (
<div>
<div style={{ marginBottom: '10px' }}>
<p>Click on the map to add markers. Click markers to remove them.</p>
<button onClick={() => setMarkers([])}>Clear All Markers</button>
</div>
<Map
initialViewState={{
longitude: -122.4,
latitude: 37.8,
zoom: 12
}}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
onClick={handleMapClick}
>
{markers.map(marker => (
<Marker
key={marker.id}
longitude={marker.longitude}
latitude={marker.latitude}
anchor="bottom"
onClick={e => {
e.originalEvent.stopPropagation();
removeMarker(marker.id);
}}
>
<div style={{
fontSize: '24px',
cursor: 'pointer'
}}>
📍
</div>
</Marker>
))}
</Map>
</div>
);
}
Advanced Features
Clustering
'use client';
import { useState } from 'react';
import Map, { Source, Layer, LayerProps } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
// Generate random points
const generatePoints = (count: number) => {
return {
type: 'FeatureCollection',
features: Array.from({ length: count }, (_, i) => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [
-122.4 + (Math.random() - 0.5) * 0.4,
37.8 + (Math.random() - 0.5) * 0.4
]
},
properties: {
id: i,
name: `Location ${i + 1}`
}
}))
};
};
export default function ClusteredMap() {
const [data] = useState(generatePoints(500));
const clusterLayer: LayerProps = {
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
10,
'#f1f075',
50,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
10,
30,
50,
40
]
}
};
const clusterCountLayer: LayerProps = {
id: 'cluster-count',
type: 'symbol',
source: 'points',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
};
const unclusteredPointLayer: LayerProps = {
id: 'unclustered-point',
type: 'circle',
source: 'points',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 6,
'circle-stroke-width': 2,
'circle-stroke-color': '#fff'
}
};
return (
<Map
initialViewState={{
longitude: -122.4,
latitude: 37.8,
zoom: 10
}}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
<Source
id="points"
type="geojson"
data={data}
cluster={true}
clusterMaxZoom={14}
clusterRadius={50}
>
<Layer {...clusterLayer} />
<Layer {...clusterCountLayer} />
<Layer {...unclusteredPointLayer} />
</Source>
</Map>
);
}
Geocoding Integration
'use client';
import { useState } from 'react';
import Map, { Marker } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
interface GeocodingResult {
center: [number, number];
place_name: string;
}
export default function GeocodingMap() {
const [searchQuery, setSearchQuery] = useState('');
const [results, setResults] = useState<GeocodingResult[]>([]);
const [selectedLocation, setSelectedLocation] = useState<GeocodingResult | null>(null);
const [viewState, setViewState] = useState({
longitude: -122.4,
latitude: 37.8,
zoom: 12
});
const searchLocation = async () => {
if (!searchQuery) return;
const token = process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
const response = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(searchQuery)}.json?access_token=${token}`
);
const data = await response.json();
setResults(data.features || []);
};
const selectLocation = (result: GeocodingResult) => {
setSelectedLocation(result);
setViewState({
longitude: result.center[0],
latitude: result.center[1],
zoom: 14
});
setResults([]);
};
return (
<div>
<div style={{ marginBottom: '10px', display: 'flex', gap: '10px' }}>
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onKeyPress={e => e.key === 'Enter' && searchLocation()}
placeholder="Search for a location..."
style={{
flex: 1,
padding: '8px',
border: '1px solid #ccc',
borderRadius: '4px'
}}
/>
<button onClick={searchLocation}>Search</button>
</div>
{results.length > 0 && (
<div style={{
marginBottom: '10px',
backgroundColor: 'white',
border: '1px solid #ccc',
borderRadius: '4px',
maxHeight: '200px',
overflowY: 'auto'
}}>
{results.map((result, index) => (
<div
key={index}
onClick={() => selectLocation(result)}
style={{
padding: '10px',
cursor: 'pointer',
borderBottom: '1px solid #eee'
}}
onMouseEnter={e => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={e => e.currentTarget.style.backgroundColor = 'white'}
>
{result.place_name}
</div>
))}
</div>
)}
<Map
{...viewState}
onMove={evt => setViewState(evt.viewState)}
style={{ width: '100%', height: '600px' }}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
{selectedLocation && (
<Marker
longitude={selectedLocation.center[0]}
latitude={selectedLocation.center[1]}
anchor="bottom"
>
<div style={{ fontSize: '30px' }}>📍</div>
</Marker>
)}
</Map>
</div>
);
}
Performance Optimization
Memoization
import { useMemo } from 'react';
import Map, { Source, Layer } from 'react-map-gl/mapbox';
export default function OptimizedMap({ data }: { data: any[] }) {
// Memoize expensive data transformations
const geojsonData = useMemo(() => {
return {
type: 'FeatureCollection',
features: data.map(item => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [item.lng, item.lat]
},
properties: item
}))
};
}, [data]);
// Memoize layer configuration
const layer = useMemo(() => ({
id: 'points',
type: 'circle',
paint: {
'circle-radius': 6,
'circle-color': '#0080ff'
}
}), []);
return (
<Map
initialViewState={{
longitude: -122.4,
latitude: 37.8,
zoom: 12
}}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
mapStyle="mapbox://styles/mapbox/streets-v12"
>
<Source type="geojson" data={geojsonData}>
<Layer {...layer} />
</Source>
</Map>
);
}
Lazy Loading
// app/map-page/page.tsx
'use client';
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
const MapComponent = dynamic(
() => import('@/components/MapComponent'),
{
ssr: false,
loading: () => (
<div style={{
width: '100%',
height: '600px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5'
}}>
Loading map...
</div>
)
}
);
export default function MapPage() {
return (
<div>
<h1>Interactive Map</h1>
<Suspense fallback={<div>Loading...</div>}>
<MapComponent />
</Suspense>
</div>
);
}
Best Practices
1. Always Use Client-Side Rendering
'use client'; // Required for Next.js App Router
2. Lazy Load Map Components
const Map = dynamic(() => import('./Map'), { ssr: false });
3. Secure API Tokens
NEXT_PUBLIC_MAPBOX_TOKEN=pk.your_token
4. Memoize Expensive Computations
const data = useMemo(() => transformData(rawData), [rawData]);
5. Clean Up Event Listeners
useEffect(() => {
const map = mapRef.current?.getMap();
const handler = () => { /* ... */ };
map?.on('click', handler);
return () => map?.off('click', handler);
}, []);
6. Handle Loading States
const [isMapLoaded, setIsMapLoaded] = useState(false);
<Map onLoad={() => setIsMapLoaded(true)} />
7. Implement Error Boundaries
<ErrorBoundary fallback={<MapErrorFallback />}>
<Map />
</ErrorBoundary>
React Map GL provides a powerful, fully reactive solution for building interactive maps in React applications. Its declarative API, excellent performance, and support for both Mapbox and MapLibre make it an excellent choice for modern web mapping applications.