tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
Solutions for complex scenarios, edge cases, and advanced React Leaflet usage.
import { MapContainer, TileLayer } from "react-leaflet";
import { useMemo, useState, useEffect } from "react";
import { useMap } from "react-leaflet";
// Use Canvas renderer for better performance
<MapContainer preferCanvas={true} center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<VirtualizedMarkers markers={largeMarkerArray} />
</MapContainer>
// Only render visible markers
function VirtualizedMarkers({ markers }) {
const map = useMap();
const [visibleMarkers, setVisibleMarkers] = useState([]);
useEffect(() => {
const updateVisibleMarkers = () => {
const bounds = map.getBounds();
const visible = markers.filter(marker =>
bounds.contains(marker.position)
);
setVisibleMarkers(visible.slice(0, 100)); // Limit to 100
};
updateVisibleMarkers();
map.on('moveend', updateVisibleMarkers);
map.on('zoomend', updateVisibleMarkers);
return () => {
map.off('moveend', updateVisibleMarkers);
map.off('zoomend', updateVisibleMarkers);
};
}, [map, markers]);
return (
<>
{visibleMarkers.map(marker => (
<Marker key={marker.id} position={marker.position} />
))}
</>
);
}import { useMapEvents } from "react-leaflet";
import { useCallback, useRef } from "react";
function DebouncedMapEvents({ onMoveEnd, delay = 300 }) {
const timeoutRef = useRef<NodeJS.Timeout>();
const debouncedMoveEnd = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
onMoveEnd();
}, delay);
}, [onMoveEnd, delay]);
useMapEvents({
moveend: debouncedMoveEnd,
});
return null;
}import { memo } from "react";
const MemoizedMarker = memo(({ position, title }) => (
<Marker position={position}>
<Popup>{title}</Popup>
</Marker>
), (prevProps, nextProps) => {
return (
prevProps.position[0] === nextProps.position[0] &&
prevProps.position[1] === nextProps.position[1] &&
prevProps.title === nextProps.title
);
});// app/map/page.tsx
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('@/components/Map'), {
ssr: false,
loading: () => (
<div style={{ height: '400px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
Loading map...
</div>
),
});
export default function MapPage() {
return (
<div>
<h1>Map Page</h1>
<Map />
</div>
);
}// pages/map.tsx
import dynamic from 'next/dynamic';
const MapWithNoSSR = dynamic(() => import('../components/Map'), {
ssr: false,
});
export default function MapPage() {
return <MapWithNoSSR />;
}// gatsby-browser.js
export const onClientEntry = () => {
// Ensure Leaflet is only loaded client-side
if (typeof window !== 'undefined') {
require('leaflet/dist/leaflet.css');
}
};
// Component
import React, { useEffect, useState } from 'react';
function Map() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return <div>Loading map...</div>;
}
const { MapContainer, TileLayer } = require('react-leaflet');
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
}function MapComponent() {
const map = useMap();
useEffect(() => {
const handleClick = (e) => {
console.log('Map clicked', e.latlng);
};
map.on('click', handleClick);
// CRITICAL: Clean up event listener
return () => {
map.off('click', handleClick);
};
}, [map]);
return null;
}function RealTimeUpdates() {
const [data, setData] = useState([]);
useEffect(() => {
const interval = setInterval(() => {
// Fetch updates
fetchData().then(setData);
}, 5000);
// CRITICAL: Clear interval on unmount
return () => {
clearInterval(interval);
};
}, []);
return <>{/* Render data */}</>;
}function CustomLayer() {
const map = useMap();
const layerRef = useRef(null);
useEffect(() => {
const layer = L.circle([51.505, -0.09], { radius: 200 });
layer.addTo(map);
layerRef.current = layer;
// CRITICAL: Remove layer on unmount
return () => {
if (layerRef.current) {
map.removeLayer(layerRef.current);
}
};
}, [map]);
return null;
}<MapContainer
center={[51.505, -0.09]}
zoom={13}
keyboard={true}
keyboardPanDelta={80}
tabIndex={0}
aria-label="Interactive map"
style={{ height: '500px' }}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>function AccessibleMap() {
const [announcements, setAnnouncements] = useState('');
return (
<div>
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcements}
</div>
<MapContainer
center={[51.505, -0.09]}
zoom={13}
aria-label="Interactive map showing locations"
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<MapEvents setAnnouncements={setAnnouncements} />
</MapContainer>
</div>
);
}
function MapEvents({ setAnnouncements }) {
useMapEvents({
zoomend: (e) => {
setAnnouncements(`Zoom level changed to ${e.target.getZoom()}`);
},
moveend: (e) => {
const center = e.target.getCenter();
setAnnouncements(`Map centered at ${center.lat.toFixed(2)}, ${center.lng.toFixed(2)}`);
},
});
return null;
}<Marker
position={[51.505, -0.09]}
title="City Hall"
alt="City Hall marker"
>
<Popup>
<div role="dialog" aria-label="City Hall information">
<h3>City Hall</h3>
<p>123 Main Street</p>
</div>
</Popup>
</Marker><MapContainer
center={[51.505, -0.09]}
zoom={13}
touchZoom={true}
tap={true}
tapTolerance={15}
dragging={true}
scrollWheelZoom={false}
doubleClickZoom={true}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>import { useState, useEffect } from 'react';
function ResponsiveMap() {
const [mapHeight, setMapHeight] = useState('400px');
useEffect(() => {
const updateHeight = () => {
if (window.innerWidth < 768) {
setMapHeight('300px');
} else if (window.innerWidth < 1024) {
setMapHeight('500px');
} else {
setMapHeight('600px');
}
};
updateHeight();
window.addEventListener('resize', updateHeight);
return () => window.removeEventListener('resize', updateHeight);
}, []);
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
style={{ height: mapHeight, width: '100%' }}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
}function ScrollControlledMap() {
const map = useMap();
useEffect(() => {
map.scrollWheelZoom.disable();
const enableZoom = () => map.scrollWheelZoom.enable();
const disableZoom = () => map.scrollWheelZoom.disable();
map.on('focus', enableZoom);
map.on('blur', disableZoom);
return () => {
map.off('focus', enableZoom);
map.off('blur', disableZoom);
};
}, [map]);
return null;
}import { render } from '@testing-library/react';
import { MapContainer, Marker } from 'react-leaflet';
// Mock Leaflet
jest.mock('leaflet', () => ({
map: jest.fn(() => ({
on: jest.fn(),
off: jest.fn(),
remove: jest.fn(),
setView: jest.fn(),
getZoom: jest.fn(() => 13),
})),
tileLayer: jest.fn(() => ({
addTo: jest.fn(),
})),
marker: jest.fn(() => ({
addTo: jest.fn(),
remove: jest.fn(),
bindPopup: jest.fn(),
})),
}));
test('renders map container', () => {
const { container } = render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<Marker position={[51.505, -0.09]} />
</MapContainer>
);
expect(container).toBeInTheDocument();
});import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('map responds to marker click', async () => {
const handleClick = jest.fn();
const { getByRole } = render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<Marker
position={[51.505, -0.09]}
eventHandlers={{ click: handleClick }}
/>
</MapContainer>
);
// Simulate interaction
await waitFor(() => {
// Add assertions
});
});import { Component, ReactNode } from 'react';
class MapErrorBoundary extends Component<
{ children: ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Map error:', error, errorInfo);
// Log to error reporting service
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
<h2>Map failed to load</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
<MapErrorBoundary>
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
</MapErrorBoundary>function RobustTileLayer({ url, fallbackUrl }) {
const [currentUrl, setCurrentUrl] = useState(url);
const [errorCount, setErrorCount] = useState(0);
const handleTileError = useCallback(() => {
setErrorCount(prev => prev + 1);
if (errorCount > 10 && fallbackUrl) {
console.warn('Switching to fallback tile server');
setCurrentUrl(fallbackUrl);
}
}, [errorCount, fallbackUrl]);
return (
<TileLayer
url={currentUrl}
eventHandlers={{
tileerror: handleTileError,
}}
/>
);
}function useMapState() {
const map = useMap();
const [state, setState] = useState({
center: map.getCenter(),
zoom: map.getZoom(),
bounds: map.getBounds(),
});
useMapEvents({
moveend: () => {
setState({
center: map.getCenter(),
zoom: map.getZoom(),
bounds: map.getBounds(),
});
},
});
return state;
}
// Usage
function MapInfo() {
const { center, zoom } = useMapState();
return (
<div>
Center: {center.lat.toFixed(4)}, {center.lng.toFixed(4)} | Zoom: {zoom}
</div>
);
}function useGeolocation() {
const map = useMap();
const [position, setPosition] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
map.locate({ setView: true, maxZoom: 16 });
const onLocationFound = (e) => {
setPosition(e.latlng);
setError(null);
};
const onLocationError = (e) => {
setError(e.message);
};
map.on('locationfound', onLocationFound);
map.on('locationerror', onLocationError);
return () => {
map.off('locationfound', onLocationFound);
map.off('locationerror', onLocationError);
};
}, [map]);
return { position, error };
}import { createContext, useContext, useState } from 'react';
const MapStateContext = createContext(null);
export function MapStateProvider({ children }) {
const [selectedFeature, setSelectedFeature] = useState(null);
const [filters, setFilters] = useState({});
const [viewState, setViewState] = useState({
center: [51.505, -0.09],
zoom: 13,
});
return (
<MapStateContext.Provider
value={{
selectedFeature,
setSelectedFeature,
filters,
setFilters,
viewState,
setViewState
}}
>
{children}
</MapStateContext.Provider>
);
}
export function useMapState() {
const context = useContext(MapStateContext);
if (!context) {
throw new Error('useMapState must be used within MapStateProvider');
}
return context;
}// Leaflet uses [lat, lng]
const leafletCoords: LatLngExpression = [51.505, -0.09];
// GeoJSON uses [lng, lat]
const geoJSONCoords = [-0.09, 51.505];
// Conversion utilities
function geoJSONToLeaflet(coords: [number, number]): [number, number] {
return [coords[1], coords[0]];
}
function leafletToGeoJSON(coords: [number, number]): [number, number] {
return [coords[1], coords[0]];
}import { useDeferredValue, useTransition } from 'react';
function ConcurrentMarkers({ markers }) {
const [isPending, startTransition] = useTransition();
const deferredMarkers = useDeferredValue(markers);
return (
<>
{isPending && <div>Updating markers...</div>}
{deferredMarkers.map(marker => (
<Marker key={marker.id} position={marker.position} />
))}
</>
);
}