tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
React Leaflet provides custom hooks for accessing the map instance and handling events from within components. These hooks must be used inside components that are children of MapContainer.
Returns the Leaflet Map instance from context.
/**
* Access the Leaflet Map instance from context
* @returns The Leaflet Map instance
* @throws Error if called outside MapContainer context
*/
function useMap(): Map;Usage Example:
import { useMap } from "react-leaflet";
import { useEffect } from "react";
function MapController() {
const map = useMap();
useEffect(() => {
// Fly to a new location
map.flyTo([51.505, -0.09], 13);
}, [map]);
return null;
}
// Use inside MapContainer
function App() {
return (
<MapContainer center={[0, 0]} zoom={2}>
<MapController />
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</MapContainer>
);
}Attaches a single event handler to the map.
/**
* Attach an event handler to the map
* @param type - Leaflet event type
* @param handler - Event handler function
* @returns The Leaflet Map instance
*/
function useMapEvent<T extends keyof LeafletEventHandlerFnMap>(
type: T,
handler: LeafletEventHandlerFnMap[T]
): Map;Usage Example:
import { useMapEvent } from "react-leaflet";
import { useState } from "react";
function ClickHandler() {
const [position, setPosition] = useState(null);
useMapEvent("click", (e) => {
setPosition(e.latlng);
console.log("Map clicked at:", e.latlng);
});
return position ? (
<div>Clicked at: {position.lat.toFixed(4)}, {position.lng.toFixed(4)}</div>
) : null;
}Common Event Types:
// Mouse events
useMapEvent("click", (e) => { /* ... */ });
useMapEvent("dblclick", (e) => { /* ... */ });
useMapEvent("mousedown", (e) => { /* ... */ });
useMapEvent("mouseup", (e) => { /* ... */ });
useMapEvent("mouseover", (e) => { /* ... */ });
useMapEvent("mouseout", (e) => { /* ... */ });
useMapEvent("mousemove", (e) => { /* ... */ });
// Map state events
useMapEvent("zoomend", (e) => { /* ... */ });
useMapEvent("moveend", (e) => { /* ... */ });
useMapEvent("dragend", (e) => { /* ... */ });
// Popup events
useMapEvent("popupopen", (e) => { /* ... */ });
useMapEvent("popupclose", (e) => { /* ... */ });Attaches multiple event handlers to the map.
/**
* Attach multiple event handlers to the map
* @param handlers - Object mapping event types to handlers
* @returns The Leaflet Map instance
*/
function useMapEvents(handlers: LeafletEventHandlerFnMap): Map;Usage Example:
import { useMapEvents } from "react-leaflet";
import { useState } from "react";
function MapEventHandler() {
const [mapState, setMapState] = useState({
zoom: 13,
center: [51.505, -0.09],
});
const map = useMapEvents({
zoomend: () => {
setMapState((prev) => ({ ...prev, zoom: map.getZoom() }));
},
moveend: () => {
setMapState((prev) => ({ ...prev, center: map.getCenter() }));
},
click: (e) => {
console.log("Clicked at:", e.latlng);
},
});
return (
<div>
Zoom: {mapState.zoom}, Center: {mapState.center.lat.toFixed(4)}, {mapState.center.lng.toFixed(4)}
</div>
);
}Multiple Events Example:
function LocationMarker() {
const [position, setPosition] = useState(null);
useMapEvents({
click(e) {
setPosition(e.latlng);
},
locationfound(e) {
setPosition(e.latlng);
map.flyTo(e.latlng, map.getZoom());
},
});
return position === null ? null : (
<Marker position={position}>
<Popup>You are here</Popup>
</Marker>
);
}// From Leaflet
interface LeafletEventHandlerFnMap {
// Layer control events
baselayerchange?: LayersControlEventHandlerFn;
overlayadd?: LayersControlEventHandlerFn;
overlayremove?: LayersControlEventHandlerFn;
// Layer events
layeradd?: LayerEventHandlerFn;
layerremove?: LayerEventHandlerFn;
// Map state events
zoomlevelschange?: EventHandlerFn;
unload?: EventHandlerFn;
viewreset?: EventHandlerFn;
load?: EventHandlerFn;
zoomstart?: EventHandlerFn;
movestart?: EventHandlerFn;
zoom?: EventHandlerFn;
move?: EventHandlerFn;
zoomend?: EventHandlerFn;
moveend?: EventHandlerFn;
// Popup and tooltip events
popupopen?: PopupEventHandlerFn;
popupclose?: PopupEventHandlerFn;
autopanstart?: EventHandlerFn;
tooltipopen?: TooltipEventHandlerFn;
tooltipclose?: TooltipEventHandlerFn;
// Mouse events
click?: LeafletMouseEventHandlerFn;
dblclick?: LeafletMouseEventHandlerFn;
mousedown?: LeafletMouseEventHandlerFn;
mouseup?: LeafletMouseEventHandlerFn;
mouseover?: LeafletMouseEventHandlerFn;
mouseout?: LeafletMouseEventHandlerFn;
mousemove?: LeafletMouseEventHandlerFn;
contextmenu?: LeafletMouseEventHandlerFn;
preclick?: LeafletMouseEventHandlerFn;
// Keyboard events
keypress?: LeafletKeyboardEventHandlerFn;
keydown?: LeafletKeyboardEventHandlerFn;
keyup?: LeafletKeyboardEventHandlerFn;
// Animation and drag events
zoomanim?: ZoomAnimEventHandlerFn;
dragstart?: EventHandlerFn;
drag?: EventHandlerFn;
dragend?: DragEndEventHandlerFn;
// Location events
locationerror?: ErrorEventHandlerFn;
locationfound?: LocationEventHandlerFn;
// Resize event
resize?: ResizeEventHandlerFn;
}
// Handler function types
type EventHandlerFn = (event: LeafletEvent) => void;
type LeafletMouseEventHandlerFn = (event: LeafletMouseEvent) => void;
type LeafletKeyboardEventHandlerFn = (event: LeafletKeyboardEvent) => void;
type PopupEventHandlerFn = (event: PopupEvent) => void;
type TooltipEventHandlerFn = (event: TooltipEvent) => void;
type LayerEventHandlerFn = (event: LayerEvent) => void;
type LayersControlEventHandlerFn = (event: LayersControlEvent) => void;
type LocationEventHandlerFn = (event: LocationEvent) => void;
type ErrorEventHandlerFn = (event: ErrorEvent) => void;
type DragEndEventHandlerFn = (event: DragEndEvent) => void;
type ZoomAnimEventHandlerFn = (event: ZoomAnimEvent) => void;
type ResizeEventHandlerFn = (event: ResizeEvent) => void;Map of all available Leaflet event types to their handler functions.
// Mouse events
interface LeafletMouseEvent {
latlng: LatLng;
layerPoint: Point;
containerPoint: Point;
originalEvent: MouseEvent;
type: string;
target: any;
}
// Location events
interface LocationEvent {
latlng: LatLng;
bounds: LatLngBounds;
accuracy: number;
altitude?: number;
altitudeAccuracy?: number;
heading?: number;
speed?: number;
timestamp: number;
}
// Drag events
interface DragEndEvent {
distance: number;
type: string;
target: any;
}Context Requirement: All hooks must be used inside components that are children of MapContainer. Using them outside will throw an error.
Cleanup: Event handlers registered with hooks are automatically cleaned up when the component unmounts.
Stable Handlers: For useMapEvent and useMapEvents, the handler functions are re-attached if they change. Use useCallback to prevent unnecessary re-registrations:
const handleClick = useCallback((e) => {
console.log(e.latlng);
}, []);
useMapEvent("click", handleClick);const map = useMapEvents({
click: (e) => {
map.setView(e.latlng, map.getZoom());
},
});Event Propagation: Events propagate from layers to the map. Use e.originalEvent.stopPropagation() to prevent this if needed.
Custom Components: These hooks are essential for creating custom components that interact with the map without rendering Leaflet layers.
function ClickToAddMarker() {
const [markers, setMarkers] = useState([]);
useMapEvent("click", (e) => {
setMarkers((current) => [...current, e.latlng]);
});
return (
<>
{markers.map((position, idx) => (
<Marker key={idx} position={position}>
<Popup>Marker {idx + 1}</Popup>
</Marker>
))}
</>
);
}function ZoomLevel() {
const map = useMap();
const [zoom, setZoom] = useState(map.getZoom());
useMapEvent("zoomend", () => {
setZoom(map.getZoom());
});
return <div className="zoom-display">Current zoom: {zoom}</div>;
}function FitBoundsOnMount({ bounds }) {
const map = useMap();
useEffect(() => {
map.fitBounds(bounds);
}, [map, bounds]);
return null;
}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 };
}
// Usage
function GeolocationMarker() {
const { position, error } = useGeolocation();
if (error) return <div>Error: {error}</div>;
if (!position) return null;
return (
<Marker position={position}>
<Popup>You are here</Popup>
</Marker>
);
}function useMapBounds() {
const map = useMap();
const [bounds, setBounds] = useState(map.getBounds());
useMapEvents({
moveend: () => {
setBounds(map.getBounds());
},
zoomend: () => {
setBounds(map.getBounds());
},
});
return bounds;
}
// Usage
function VisibleAreaInfo() {
const bounds = useMapBounds();
const area = bounds.toBBoxString();
return <div>Visible area: {area}</div>;
}function useDebouncedMapEvent(eventType, handler, delay = 300) {
const map = useMap();
const timeoutRef = useRef(null);
useEffect(() => {
const debouncedHandler = (e) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
handler(e);
}, delay);
};
map.on(eventType, debouncedHandler);
return () => {
map.off(eventType, debouncedHandler);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [map, eventType, handler, delay]);
}
// Usage
function DebouncedSearch() {
const handleMoveEnd = useCallback(() => {
console.log("Map stopped moving");
// Perform expensive operation
}, []);
useDebouncedMapEvent("moveend", handleMoveEnd, 500);
return null;
}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(),
});
},
zoomend: () => {
setState({
center: map.getCenter(),
zoom: map.getZoom(),
bounds: map.getBounds(),
});
},
});
return state;
}
// Usage
function MapStateDisplay() {
const { center, zoom, bounds } = useMapState();
return (
<div>
<p>Center: {center.lat.toFixed(4)}, {center.lng.toFixed(4)}</p>
<p>Zoom: {zoom}</p>
</div>
);
}function useClickPosition() {
const [position, setPosition] = useState(null);
useMapEvent("click", (e) => {
setPosition(e.latlng);
});
return position;
}
// Usage
function ClickPositionMarker() {
const position = useClickPosition();
return position ? (
<Marker position={position}>
<Popup>
Clicked at: {position.lat.toFixed(4)}, {position.lng.toFixed(4)}
</Popup>
</Marker>
) : null;
}function useThrottledMapEvent(eventType, handler, delay = 100) {
const map = useMap();
const lastCall = useRef(0);
useEffect(() => {
const throttledHandler = (e) => {
const now = Date.now();
if (now - lastCall.current >= delay) {
lastCall.current = now;
handler(e);
}
};
map.on(eventType, throttledHandler);
return () => {
map.off(eventType, throttledHandler);
};
}, [map, eventType, handler, delay]);
}function OptimizedMapEvents() {
const handleClick = useCallback((e) => {
console.log("Clicked at:", e.latlng);
}, []);
const handleZoom = useCallback(() => {
console.log("Zoom changed");
}, []);
useMapEvents({
click: handleClick,
zoomend: handleZoom,
});
return null;
}Error: "No context provided: useLeafletContext() can only be used in a descendant of <MapContainer>"
Solution: Ensure component using hooks is rendered inside MapContainer:
<MapContainer center={[51.505, -0.09]} zoom={13}>
<ComponentUsingHooks />
</MapContainer>Solutions:
Solutions:
import { renderHook } from '@testing-library/react';
import { useMap } from 'react-leaflet';
import { MapContainer } from 'react-leaflet';
test('useMap returns map instance', () => {
const wrapper = ({ children }) => (
<MapContainer center={[51.505, -0.09]} zoom={13}>
{children}
</MapContainer>
);
const { result } = renderHook(() => useMap(), { wrapper });
expect(result.current).toBeDefined();
expect(result.current.getZoom).toBeDefined();
});test('useMapEvent attaches event handler', () => {
const handleClick = jest.fn();
function TestComponent() {
useMapEvent('click', handleClick);
return null;
}
render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TestComponent />
</MapContainer>
);
// Simulate map click and verify handler called
});import { useMap, useMapEvents } from 'react-leaflet';
import type { Map, LeafletMouseEvent } from 'leaflet';
function TypeSafeMapComponent() {
// Explicit type annotation
const map: Map = useMap();
// Type-safe event handlers
const eventMap = useMapEvents({
click: (e: LeafletMouseEvent) => {
console.log('Clicked at:', e.latlng.lat, e.latlng.lng);
},
zoomend: () => {
const zoom: number = map.getZoom();
console.log('Zoom level:', zoom);
},
});
return null;
}function AccessibleMapControl() {
const map = useMap();
const handleFocusMap = useCallback(() => {
const container = map.getContainer();
container.focus();
container.setAttribute('aria-label', 'Map focused. Use arrow keys to pan.');
}, [map]);
return (
<button
onClick={handleFocusMap}
aria-label="Focus map for keyboard navigation"
>
Focus Map
</button>
);
}