react-leaflet review
React Leaflet 5 wraps Leaflet 1.9 maps, layers, markers, popups, controls, and events in React 19 components and hooks. Our browser build was 170.2 KB minified and 49.8 KB gzipped. React manages component lifecycle while Leaflet owns the map objects and map DOM. Version 5 moves the peer floor to React 19 and removes `LeafletProvider` from the core package. It supplies no tiles, geocoding, routing, offline store, or server-rendered map.
React Leaflet 5 installed in 2 seconds and built to 170.2 KB minified in our sandbox, while direct `require()` and ESM `import` both failed under Node.js v22.23.2. Install it for client-only React 19 maps built on Leaflet; walk away for SSR, React 18, an automatic controlled-prop model, or an unapproved Hippocratic-2.1 license.
We installed it
| Install | ✓ · 2s | 6 packages on disk · 12 MB |
| Import | ✗ | ESM import fails · require() fails · ESM package with exports map |
| Browser | 49.8 KB | gzipped (170.2 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does react-leaflet install cleanly?
Yes. In a fresh container with an empty cache, npm install react-leaflet finished in 2 seconds, leaving 6 packages and 12 MB on disk. npm audit reported no known vulnerabilities.
How much does react-leaflet add to a browser bundle?
49.8 KB gzipped (170.2 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does react-leaflet work with both ESM and CommonJS?
Neither plain import nor require succeeded in our sandbox, so it needs a bundler or extra setup.
Does react-leaflet include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
react-leaflet or react-map-gl: which should you use?
react-map-gl: Use it for React bindings around MapLibre or Mapbox GL when vector styles, bearing, and pitch matter. React Leaflet 5 installed in 2 seconds and built to 170.2 KB minified in our sandbox, while direct require() and ESM import both failed under Node.js v22.23.2.
When should you not use react-leaflet?
Server rendering must execute the map module: the official introduction says Leaflet touches the DOM while loading and React Leaflet is incompatible with SSR
Use it if
- A React 19 application needs classic raster tiles, markers, popups, GeoJSON, and vector overlays
- Leaflet's plugin and provider ecosystem is already part of the mapping plan
- Imperative camera changes through `useMap`, refs, `setView`, or `fitBounds` fit the component design
- A DOM-based map is enough and WebGL pitch, globe views, or large vector-tile scenes are unnecessary
- Server rendering must execute the map module: the official introduction says Leaflet touches the DOM while loading and React Leaflet is incompatible with SSR
- The app is on React 18 or earlier: version 5 peers require React 19, React DOM 19, and Leaflet 1.9
- Dependency policy accepts only routine permissive licenses: the package uses Hippocratic License 2.1 with use conditions that need legal review
- Map creation options should behave like controlled React props: `MapContainer` ignores later `center`, `zoom`, and bounds changes
- A small browser payload is mandatory: our full-package import produced 170.2 KB minified and 49.8 KB gzipped
Setup reality
Our fresh install of react-leaflet 5.0.0 took 2 seconds and left 6 packages using 12 MB on disk. The package itself has one direct dependency and 3 peers, bundles TypeScript declarations, and produced 0 known audit vulnerabilities. Its unpacked size is 220 KB under Hippocratic-2.1.
Install matching peers for React 19, React DOM 19, and Leaflet 1.9. Import leaflet/dist/leaflet.css and assign a real height to MapContainer; missing either commonly leaves a blank or broken map box. Leaflet declarations may still require @types/leaflet. Choose a tile provider, preserve required attribution, follow its traffic policy, and supply any API token through your own configuration.
The package declares ESM and has an exports map, yet both require() and ESM import failed in our direct Node.js v22.23.2 checks. The browser-oriented esbuild path succeeded at 170.2 KB minified and 49.8 KB gzipped. Keep imports inside a client-only boundary. In Next.js, dynamically load the map with server rendering disabled so a server-evaluated module never loads Leaflet.
MapContainer uses center, zoom, bounds, and other creation options only on first render. Move the camera later through useMap or a ref. Component documentation marks the smaller set of mutable props; changing other values may require a new React key to recreate the layer. Default marker images can break when a bundler relocates Leaflet assets, so import image files and construct an explicit Icon. Review Hippocratic-2.1 before shipping because its obligations differ from MIT or BSD terms.
Patterns
Mount a map with visible tiles render-tile-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 height are both required. Check the tile provider's production traffic and attribution policy.
Attach a popup to a marker add-marker-popup
import { Marker, Popup } from 'react-leaflet'
<Marker position={[51.505, -0.09]}>
<Popup>Central London</Popup>
</Marker>Render this below `MapContainer` so context can supply the map. Default marker image URLs may need explicit bundler handling.
React to clicks and location results handle-map-events
function LocationMarker() {
const [position, setPosition] = useState(null)
const map = useMapEvents({
click() { map.locate() },
locationfound(event) {
setPosition(event.latlng)
map.flyTo(event.latlng, map.getZoom())
},
locationerror(error) { console.error(error) },
})
return position ? <Marker position={position} /> : null
}Browser geolocation generally needs HTTPS and user permission. Handle `locationerror` so denial does not look like a dead map.
Move the camera after mount update-map-view
function ViewController({ center, zoom }) {
const map = useMap()
useEffect(() => {
map.setView(center, zoom)
}, [map, center, zoom])
return null
}Changing `MapContainer` center or zoom later does nothing because those creation options are immutable.
Fit the viewport around data fit-feature-bounds
function FitBounds({ bounds }) {
const map = useMap()
useEffect(() => {
if (bounds) map.fitBounds(bounds, { padding: [24, 24] })
}, [map, bounds])
return null
}Memoize `bounds` when possible. A fresh array each render can repeatedly reset a user's camera.
Style and label GeoJSON render-geojson
<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')
}
/>Version 5 does not mark GeoJSON `data` mutable. Change the component key when replacing the full collection.
Offer two tile sources switch-base-layers
<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>Example tile URLs are external services with separate capacity and attribution terms, not bundled React Leaflet assets.
Import marker images through the bundler use-custom-marker-icon
import { Icon } from 'leaflet'
import markerUrl from './marker.png'
import shadowUrl from './marker-shadow.png'
const icon = new Icon({
iconUrl: markerUrl, shadowUrl,
iconSize: [25, 41], iconAnchor: [12, 41],
})
<Marker position={[51.505, -0.09]} icon={icon} />An explicit `Icon` avoids broken defaults after a bundler moves Leaflet's package images.
Store a marker's dragged position track-dragged-marker
function DraggableMarker() {
const [position, setPosition] = useState([51.505, -0.09])
const ref = useRef(null)
const eventHandlers = useMemo(() => ({
dragend() { if (ref.current) setPosition(ref.current.getLatLng()) },
}), [])
return <Marker ref={ref} draggable position={position} eventHandlers={eventHandlers} />
}The ref exposes a Leaflet `Marker`, not a DOM element. Memoizing handlers avoids replacing listeners each render.
Draw a radius and route draw-vector-layers
<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. Check the component reference before assuming another Leaflet constructor option updates.
Control vector stacking order set-layer-pane
<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 one map namespace. `pointerEvents: 'none'` lets gestures reach the map below the overlay.
Reset the camera from a button access-map-ref
function MapWithReset() {
const mapRef = useRef(null)
return <>
<button onClick={() => mapRef.current?.setView([51.505, -0.09], 13)}>Reset</button>
<MapContainer ref={mapRef} center={[51.505, -0.09]} zoom={13} style={{ height: 420 }}>
{/* layers */}
</MapContainer>
</>
}Avoid multiple components issuing camera commands at once. Give one controller ownership of map movement.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-map-gl | npm | Use it for React bindings around MapLibre or Mapbox GL when vector styles, bearing, and pitch matter |
| maplibre-gl | npm | Use it directly for an imperative WebGL vector map without a React component abstraction |
| leaflet | npm | Use Leaflet alone when React lifecycle wrappers add no value or another framework owns the UI |
More web frontend guides
postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

