react-leaflet
React Leaflet turns Leaflet maps, tile layers, markers, popups, controls and vector layers into React components and supplies hooks for the underlying Leaflet map. React owns the component lifecycle, while Leaflet still creates and updates the actual map layers and DOM. Version 5 targets React 19 and Leaflet 1.9. It is a binding, not a tile service, geocoder, routing engine, offline map store or server-rendered map solution.
Still the cleanest React binding for classic Leaflet maps, especially markers, raster tiles and modest GeoJSON. Install it only after accepting the React 19 floor, client-only rendering, immutable-prop model and nonstandard license.
Use it if
- Your application already uses React 19 and you want the mature Leaflet layer model expressed as React components
- You need markers, popups, GeoJSON, tile layers and simple vector overlays without adopting a WebGL-first map stack
- You are comfortable using Leaflet's imperative map API through useMap for interactions that props do not update
- You want to use the broader Leaflet plugin ecosystem and can wrap plugin lifecycle behavior where a React adapter is absent
- You need server-side rendering: the official introduction says Leaflet calls the DOM when loaded and React Leaflet is not compatible with SSR
- Your project is on React 18 or earlier: version 5 declares React 19 and React DOM 19 as required peer dependencies; version 4 is a separate older line
- Your compliance policy accepts only OSI-approved permissive licenses: this project uses Hippocratic License 2.1 with human-rights use conditions, indemnity and arbitration terms that require organizational review
- You expect ordinary controlled React props: MapContainer options are immutable after the first render, and most child props update only when the API reference explicitly marks them mutable
- You need globe rendering, vector-tile styling, pitch, bearing or GPU-heavy datasets: Leaflet's DOM and raster-first model is a worse fit than MapLibre GL for those jobs
Setup reality
Install react-leaflet 5 with its required peers: react ^19, react-dom ^19 and leaflet ^1.9. TypeScript definitions ship with React Leaflet, but Leaflet's definitions are separate, so TypeScript projects also install @types/leaflet. Import leaflet/dist/leaflet.css and give the map container a real height; without either, the common result is a blank or broken-looking box even though the component mounted. The package is ESM-only, and the version 5 docs warn that TypeScript declarations are exported from the package entry point, not deep paths such as react-leaflet/MapContainer. Browser rendering is mandatory because Leaflet touches the DOM as it loads. In Next.js or another SSR framework, isolate the map in a client-only boundary and dynamically import it with server rendering disabled; simply adding a client directive may not be enough if a server-evaluated module imports Leaflet. MapContainer's center, zoom, bounds and other creation options do not become controlled props, so later camera changes go through useMap, a ref, setView, flyTo or fitBounds. Most layer props are also immutable unless the reference marks them mutable, which can require changing a React key to recreate a layer. You must choose a tile provider, honor its attribution and usage policy, and supply any API token yourself. Public OpenStreetMap tiles are fine for light development but are not an unlimited production CDN. Default Leaflet marker icons can break when a bundler relocates their image files; creating an explicit Icon from imported assets avoids that surprise. Finally, the Hippocratic-2.1 license is not a routine MIT-style permission grant, so have the person responsible for dependency policy read it before shipping.
Patterns
Render a tile maprender-basic-map
import 'leaflet/dist/leaflet.css'
import { MapContainer, TileLayer } from 'react-leaflet'
export function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: 420 }}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
)
}Leaflet CSS and a nonzero container height are both required. Check the tile provider's production usage and attribution terms before launch.
Add a marker with a popupadd-marker-popup
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: 420 }}>
<TileLayer attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker position={[51.505, -0.09]}>
<Popup>Central London</Popup>
</Marker>
</MapContainer>Marker must be below MapContainer because React Leaflet gets the Leaflet map through context. Default icon URLs may need explicit bundler handling.
Handle clicks and geolocation eventshandle-map-events
import { useState } from 'react'
import { Marker, Popup, useMapEvents } from 'react-leaflet'
function LocationMarker() {
const [position, setPosition] = useState(null)
const map = useMapEvents({
click() {
map.locate()
},
locationfound(event) {
setPosition(event.latlng)
map.flyTo(event.latlng, map.getZoom())
},
})
return position ? <Marker position={position}><Popup>You are here</Popup></Marker> : null
}Browser geolocation normally requires HTTPS and user permission. Handle locationerror as well in production instead of leaving the click unexplained.
Move the map when React state changesupdate-map-view
import { useEffect } from 'react'
import { useMap } from 'react-leaflet'
function ViewController({ center, zoom }) {
const map = useMap()
useEffect(() => {
map.setView(center, zoom)
}, [map, center, zoom])
return null
}
// Render <ViewController center={center} zoom={zoom} /> inside MapContainer.Changing MapContainer center or zoom after mount has no effect because its creation props are immutable. Use the Leaflet map API instead.
Fit the camera to boundsfit-feature-bounds
import { useEffect } from 'react'
import { useMap } from 'react-leaflet'
function FitBounds({ bounds }) {
const map = useMap()
useEffect(() => {
if (bounds) map.fitBounds(bounds, { padding: [24, 24] })
}, [map, bounds])
return null
}Memoize bounds in the parent when possible. A newly allocated array on every render retriggers the effect and can keep resetting the user's view.
Render and style GeoJSONrender-geojson
import { GeoJSON } from 'react-leaflet'
<GeoJSON
data={featureCollection}
style={(feature) => ({
color: feature?.properties?.selected ? '#dc2626' : '#2563eb',
weight: 2,
fillOpacity: 0.25,
})}
onEachFeature={(feature, layer) => {
layer.bindTooltip(feature.properties?.name ?? 'Unnamed area')
}}
/>GeoJSON data is not documented as mutable in version 5. Change the component key when replacing the whole dataset so Leaflet recreates the layer.
Switch base layersadd-layer-control
import { LayersControl, TileLayer } from 'react-leaflet'
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Street">
<TileLayer attribution='© OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Humanitarian">
<TileLayer attribution='© OpenStreetMap contributors, Tiles style by HOT' url="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png" />
</LayersControl.BaseLayer>
</LayersControl>Every provider has its own availability, attribution and rate policy. Treat example URLs as configuration to review, not a guaranteed production service.
Use bundler-managed marker assetsuse-custom-marker-icon
import { Icon } from 'leaflet'
import markerUrl from './marker.png'
import marker2xUrl from './marker@2x.png'
import shadowUrl from './marker-shadow.png'
import { Marker } from 'react-leaflet'
const icon = new Icon({
iconUrl: markerUrl,
iconRetinaUrl: marker2xUrl,
shadowUrl,
iconSize: [25, 41],
iconAnchor: [12, 41],
})
<Marker position={[51.505, -0.09]} icon={icon} />Creating an explicit icon avoids the common broken-image problem when a bundler moves Leaflet's package assets. Configure image imports for your build tool.
Read a dragged marker positiondrag-marker
import { useMemo, useRef, useState } from 'react'
import { Marker } from 'react-leaflet'
function DraggableMarker() {
const [position, setPosition] = useState([51.505, -0.09])
const markerRef = useRef(null)
const handlers = useMemo(() => ({
dragend() {
const marker = markerRef.current
if (marker) setPosition(marker.getLatLng())
},
}), [])
return <Marker draggable eventHandlers={handlers} position={position} ref={markerRef} />
}The ref exposes the Leaflet Marker instance, not a DOM node. Memoizing eventHandlers avoids unnecessary listener replacement.
Draw circles and polylinesdraw-vector-layers
import { Circle, Polyline } from 'react-leaflet'
<Circle
center={[51.505, -0.09]}
radius={250}
pathOptions={{ color: '#7c3aed', fillOpacity: 0.2 }}
/>
<Polyline
positions={[[51.50, -0.10], [51.505, -0.09], [51.51, -0.08]]}
pathOptions={{ color: '#0f766e', weight: 4 }}
/>pathOptions is mutable, but not every Leaflet constructor option is. Check the version 5 component table before expecting a changed prop to update.
Put layers in a custom panecontrol-layer-order
import { Circle, Pane } from 'react-leaflet'
<Pane name="alerts" style={{ zIndex: 650, pointerEvents: 'none' }}>
<Circle
center={[51.505, -0.09]}
radius={500}
pathOptions={{ color: '#dc2626', fillOpacity: 0.15 }}
/>
</Pane>Pane names share a map-wide namespace. Pick stable unique names, and remember that pointer-events settings affect whether underlying map gestures work.
Expose the Leaflet map with a refaccess-map-ref
import { useRef } from 'react'
import { MapContainer } from 'react-leaflet'
function MapWithReset() {
const mapRef = useRef(null)
return (
<>
<button onClick={() => mapRef.current?.setView([51.505, -0.09], 13)}>Reset view</button>
<MapContainer ref={mapRef} center={[51.505, -0.09]} zoom={13} style={{ height: 420 }}>
{/* layers */}
</MapContainer>
</>
)
}Imperative calls can conflict with child controllers that also change the camera. Give one part of the component tree ownership of map movement.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @vis.gl/react-maplibre | npm | You want React bindings for GPU-rendered vector maps, pitch, bearing and MapLibre styles under an MIT license |
| react-map-gl | npm | You use the vis.gl React API and may target Mapbox GL or MapLibre through its supported entry points |
| @react-google-maps/api | npm | Google Maps services, places and its commercial data are requirements and you accept API keys and usage billing |
| maplibre-gl | npm | You prefer direct imperative control over a WebGL map and do not need a React component wrapper |