mrkeyoor.com_
Sat 08 Aug 21:56 UTC
npmWeb Frontendupdated 08 Aug 2026

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.

Verdict

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.

API stability3/5The component names and useMap hooks are small and recognizable, but major upgrades have made concrete breaking changes. Version 5 requires React 19 and removes LeafletProvider from the core package. Version 4 previously removed CommonJS and UMD builds, MapConsumer, useMapElement, whenCreated and popup lifecycle props. The documented rule that most props are immutable also means normal React state changes do not always produce the update a caller expects.
Docs4/5The versioned site clearly explains the React and Leaflet split, lifecycle, SSR limitation, immutable props, required CSS, container height, hooks and each component's mutable fields. It also includes focused working examples for events, bounds, layers, panes and external state. The installation page still shows release-candidate and next tags even though version 5 is stable, and many Leaflet options require a second trip to Leaflet's own reference.
Maintenance3/5Version 5.0.0 was released in December 2024 and the repository was last pushed in December 2025, so the project is maintained but not shipping frequently as of August 2026. The repository reports 48 open issues and pull requests and directs support questions to Stack Overflow. A quiet wrapper around stable Leaflet does not need weekly releases, but teams should not expect rapid compatibility work or a large maintainer operation.
Ecosystem4/5The package records 3,589,491 downloads for the measured week and sits on Leaflet 1.9, whose plugin and tile-provider ecosystem covers many common mapping jobs. React Leaflet ships TypeScript declarations, standard hooks and wrappers for markers, GeoJSON, controls, panes and overlays. Plugin quality varies, React adapters are not guaranteed, and the Hippocratic-2.1 license can exclude organizations whose dependency policies require OSI-approved licenses.

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
Skip it if

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='&copy; 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='&copy; 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='&copy; OpenStreetMap contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
  </LayersControl.BaseLayer>
  <LayersControl.BaseLayer name="Humanitarian">
    <TileLayer attribution='&copy; 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

PackageRegistryPick it when
@vis.gl/react-maplibrenpmYou want React bindings for GPU-rendered vector maps, pitch, bearing and MapLibre styles under an MIT license
react-map-glnpmYou use the vis.gl React API and may target Mapbox GL or MapLibre through its supported entry points
@react-google-maps/apinpmGoogle Maps services, places and its commercial data are requirements and you accept API keys and usage billing
maplibre-glnpmYou prefer direct imperative control over a WebGL map and do not need a React component wrapper