@react-leaflet/core
`@react-leaflet/core` is the low-level construction kit behind React Leaflet 5. It exposes React context, lifecycle hooks, update helpers, and component factories for turning Leaflet classes or third-party Leaflet plugins into declarative React components. Most application developers should install and use `react-leaflet` instead; this package is for adapter authors and teams whose custom map behavior does not fit the public components.
Use this package to publish serious React 19 adapters for Leaflet plugins; its factories encode the lifecycle details that are easy to get wrong. Application code that just needs a map should stay on `react-leaflet`, and React 18 or CommonJS projects should not force core 3 into the build.
Use it if
- You are wrapping a third-party Leaflet layer or control as a reusable React component
- You need React Leaflet to add, update, and remove a custom Leaflet object with the same lifecycle as built-in layers
- You are building a component library on React 19, Leaflet 1.9, and React Leaflet 5
- You need direct access to the map, parent layer container, pane, or overlay context below `MapContainer`
- You only want to place markers, tiles, popups, shapes, or controls on a map: the core documentation directs ordinary users to the public `react-leaflet` API
- Your application is on React 18: version 3.0.0 declares React 19 and React DOM 19 as peer dependencies, matching React Leaflet 5's breaking change
- Your build still consumes CommonJS: the package is ESM-only with `type: module` and one ESM export, while React Leaflet removed CommonJS and UMD distributions in version 4
- You need a permissive OSI-style license approved by a conservative legal policy: the package declares Hippocratic License 2.1, whose ethical-use conditions differ from MIT, BSD, or Apache terms
- You do not already understand Leaflet lifecycle and React hooks: factories remove boilerplate, but you still must decide how an instance is created, which props are mutable, what context children receive, and how plugin resources are cleaned up
Setup reality
Install `@react-leaflet/core`, `react-leaflet`, `leaflet`, `react`, and `react-dom` in compatible generations. Core 3.0.0 requires Leaflet `^1.9.0` plus React and React DOM `^19.0.0`; npm can warn or fail when an existing React 18 application tries to add it. Leaflet 1.9.4 does not publish its own TypeScript declarations, while core's declarations import Leaflet types, so TypeScript projects normally add `@types/leaflet` as a development dependency. The package ships ESM only. Jest, older bundlers, and server code configured for CommonJS need ESM handling instead of `require()`. Importing map code during server rendering is another common surprise because Leaflet is browser and DOM oriented; put the map behind the framework's client boundary and, where necessary, load it only in the browser. Core does not replace the usual Leaflet setup: the map container needs an explicit height and normal Leaflet UI still needs `leaflet/dist/leaflet.css`. Every core hook also assumes React Leaflet context. `useLeafletContext()` throws when called outside a descendant of `MapContainer`. For a wrapper, creation runs once, so props only become mutable if you supply an update function. Factories handle standard add/remove, events, paths, panes, and refs, but plugin-specific listeners, workers, canvases, or DOM nodes remain your cleanup responsibility. Finally, review the Hippocratic 2.1 license before distributing a product built on it; do not assume it has the same approval profile as Leaflet's BSD license.
Patterns
Turn a Leaflet control into a React componentwrap-leaflet-control
import { createControlComponent } from '@react-leaflet/core';
import { Control } from 'leaflet';
export const ZoomControl = createControlComponent<
Control.Zoom,
Control.ZoomOptions
>((props) => new Control.Zoom(props));The control factory adds and removes the instance and updates its position. Render it below `MapContainer`, not as a standalone component.
Create an updatable path componentwrap-custom-path
import { createElementObject, createPathComponent, type PathProps } from '@react-leaflet/core';
import { Rectangle, latLng, type LatLngExpression } from 'leaflet';
type SquareProps = PathProps & { center: LatLngExpression; size: number };
const bounds = (p: SquareProps) => latLng(p.center).toBounds(p.size);
export const Square = createPathComponent<Rectangle, SquareProps>(
(props, context) => createElementObject(
new Rectangle(bounds(props), props.pathOptions), context
),
(layer, props, prev) => {
if (props.center !== prev.center || props.size !== prev.size) {
layer.setBounds(bounds(props));
}
}
);Creation runs once. The second callback is what makes `center` and `size` changes reach the existing Leaflet rectangle.
Wrap a custom tile source with mutable optionswrap-tile-layer
import { createElementObject, createTileLayerComponent, updateGridLayer, type LayerProps } from '@react-leaflet/core';
import { TileLayer, type TileLayerOptions } from 'leaflet';
type Props = LayerProps & TileLayerOptions & { url: string };
export const CustomTiles = createTileLayerComponent<TileLayer, Props>(
({ url, eventHandlers, ...options }, context) =>
createElementObject(new TileLayer(url, options), context),
(layer, props, prev) => {
updateGridLayer(layer, props, prev);
if (props.url !== prev.url) layer.setUrl(props.url);
}
);`updateGridLayer` only covers opacity and z-index. Update the URL yourself, and keep React-only props out of Leaflet's constructor options.
Access the current Leaflet map in a custom hookread-map-context
import { useLeafletContext } from '@react-leaflet/core';
import { useEffect } from 'react';
export function useFitOnLoad() {
const { map } = useLeafletContext();
useEffect(() => {
map.fitBounds([[40.70, -74.02], [40.88, -73.90]]);
}, [map]);
}The hook throws when no React Leaflet context exists, so call it only from a component rendered beneath `MapContainer`.
Build a layer from low-level hooksmanage-layer-lifecycle
import { createElementHook, createElementObject, useLayerLifecycle, useLeafletContext } from '@react-leaflet/core';
import { Marker, type LatLngExpression } from 'leaflet';
const useMarkerElement = createElementHook(
({ position }: { position: LatLngExpression }, context) =>
createElementObject(new Marker(position), context),
(marker, props, prev) => {
if (props.position !== prev.position) marker.setLatLng(props.position);
}
);
export function BareMarker(props: { position: LatLngExpression }) {
const context = useLeafletContext();
const element = useMarkerElement(props, context);
useLayerLifecycle(element.current, context);
return null;
}Prefer `createLayerComponent` for ordinary wrappers. These lower-level pieces are useful only when your component lifecycle needs extra behavior.
Call Leaflet methods through a forwarded refforward-layer-ref
import { useEffect, useRef } from 'react';
import type { Rectangle } from 'leaflet';
function Selection() {
const rectangleRef = useRef<Rectangle>(null);
useEffect(() => {
rectangleRef.current?.bringToFront();
}, []);
return <Square ref={rectangleRef} center={[51.5, -0.09]} size={500} />;
}Components created by the core factories forward the underlying Leaflet instance, not a DOM element.
Expose Leaflet events through component propshandle-layer-events
<Square
center={[51.5, -0.09]}
size={500}
pathOptions={{ color: 'tomato' }}
eventHandlers={{
click: (event) => console.log(event.latlng),
mouseover: (event) => event.target.setStyle({ weight: 6 }),
}}
/>`createPathComponent` wires `eventHandlers` on mount and removes the previous handler map when the prop changes or the component unmounts.
Route child layers into a custom groupprovide-child-container
import { createElementObject, createLayerComponent, extendContext, type LayerProps } from '@react-leaflet/core';
import { FeatureGroup } from 'leaflet';
import type { PropsWithChildren } from 'react';
type GroupProps = LayerProps & PropsWithChildren;
export const PluginGroup = createLayerComponent<FeatureGroup, GroupProps>(
(props, context) => {
const group = new FeatureGroup();
return createElementObject(
group,
extendContext(context, { layerContainer: group })
);
}
);The extended context makes nested React Leaflet layers attach to the group. Context objects are frozen, so create an extension instead of mutating the parent.
Reuse the shared media overlay updaterupdate-media-overlay
import { createElementObject, createLayerComponent, updateMediaOverlay, type MediaOverlayProps } from '@react-leaflet/core';
import { ImageOverlay } from 'leaflet';
type Props = MediaOverlayProps & { url: string };
export const PhotoOverlay = createLayerComponent<ImageOverlay, Props>(
({ url, bounds, eventHandlers, children, ...options }, context) =>
createElementObject(new ImageOverlay(url, bounds, options), context),
(overlay, props, prev) => {
updateMediaOverlay(overlay, props, prev);
if (props.url !== prev.url) overlay.setUrl(props.url);
}
);The helper updates bounds, opacity, and z-index, but it does not update your custom URL prop.
Replace classes on plugin-owned DOMupdate-control-classes
import { updateClassName } from '@react-leaflet/core';
function syncControlTheme(
container: HTMLElement,
previousTheme?: string,
nextTheme?: string
) {
updateClassName(container, previousTheme, nextTheme);
}The helper splits space-separated class strings and uses Leaflet's DOM utility. Pass the previous and next values or stale classes remain.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-leaflet | npm | You are building a map screen and need the supported public components rather than adapter internals |
| react-map-gl | npm | You need React bindings for Mapbox GL or MapLibre vector maps instead of the Leaflet plugin ecosystem |
| @react-google-maps/api | npm | Your product is committed to Google Maps services and wants React components for that API |