Leaflet
"Leaflet: open-source maps, markers, popups, layers, GeoJSON, plugins, React (react-leaflet), tile providers"
Leaflet is a lightweight, open-source mapping library that prioritizes simplicity, performance, and extensibility. At ~42KB gzipped, it delivers a complete mapping experience without the weight of commercial SDKs. Leaflet renders raster tiles by default and does not require WebGL, making it compatible with virtually every browser and device. Its plugin ecosystem fills gaps the core intentionally leaves out -- clustering, heatmaps, routing, drawing -- keeping the base library focused. In React, use `react-leaflet` v4+ which provides a declarative component API while preserving access to the underlying Leaflet instances through refs and the `useMap` hook. Leaflet is tile-provider-agnostic: you can use OpenStreetMap, Mapbox, Stadia Maps, Thunderforest, or your own tile server. ## Key Points - **Fix marker icons early**: Bundlers (Webpack, Vite) break Leaflet's default icon paths. Apply the `mergeOptions` fix at app initialization, not inside components. - **Use `whenReady` or `useMap` for post-init logic**: Never access the map instance before it finishes initializing. The `useMap` hook guarantees the instance is ready. - **Key GeoJSON components for re-rendering**: `react-leaflet`'s `<GeoJSON>` does not update when data changes. Pass a unique `key` prop to force remount when the data object changes. - **Respect tile provider terms**: OpenStreetMap's tile servers have a usage policy. For production traffic, use a commercial provider or host your own tile server. - **Debounce viewport-dependent data fetches**: Listen to `moveend` (not `move`) and debounce to avoid overwhelming your API on every pan frame. - **Set `maxBounds` for constrained maps**: If your app only covers a specific region, set `maxBounds` on the `MapContainer` to prevent users from scrolling to irrelevant areas. - **Including Leaflet CSS only in the component file**: The CSS must be loaded globally or at the layout level. Missing CSS causes tiles to render in a broken grid pattern. - **Creating L.map() manually in React**: Never call `L.map()` yourself alongside `react-leaflet`. Use `<MapContainer>` exclusively -- it owns the Leaflet instance lifecycle. - **Ignoring the `key` prop on GeoJSON**: Without a fresh key, the GeoJSON layer keeps stale data. This is the most common bug in react-leaflet apps. - **Loading heavy plugins unconditionally**: Plugins like `leaflet-draw` and `leaflet.heat` add significant bundle weight. Dynamic-import them only on pages that use drawing or heatmap features.
skilldb get maps-geolocation-services-skills/LeafletFull skill: 262 linesLeaflet
Core Philosophy
Leaflet is a lightweight, open-source mapping library that prioritizes simplicity, performance, and extensibility. At ~42KB gzipped, it delivers a complete mapping experience without the weight of commercial SDKs. Leaflet renders raster tiles by default and does not require WebGL, making it compatible with virtually every browser and device. Its plugin ecosystem fills gaps the core intentionally leaves out -- clustering, heatmaps, routing, drawing -- keeping the base library focused. In React, use react-leaflet v4+ which provides a declarative component API while preserving access to the underlying Leaflet instances through refs and the useMap hook. Leaflet is tile-provider-agnostic: you can use OpenStreetMap, Mapbox, Stadia Maps, Thunderforest, or your own tile server.
Setup
// npm install leaflet react-leaflet @types/leaflet
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
// Fix default marker icon paths (common issue with bundlers)
import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerShadow from "leaflet/dist/images/marker-shadow.png";
L.Icon.Default.mergeOptions({
iconRetinaUrl: markerIcon2x,
iconUrl: markerIcon,
shadowUrl: markerShadow,
});
Basic map component:
function BasicMap() {
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
style={{ height: "100vh", width: "100%" }}
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
);
}
Key Techniques
Custom Markers with Icons
const coffeeIcon = L.icon({
iconUrl: "/icons/coffee.png",
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
function CoffeeShopMarkers({ shops }: { shops: Array<{ id: string; name: string; lat: number; lng: number }> }) {
return (
<>
{shops.map((shop) => (
<Marker key={shop.id} position={[shop.lat, shop.lng]} icon={coffeeIcon}>
<Popup>
<strong>{shop.name}</strong>
</Popup>
</Marker>
))}
</>
);
}
GeoJSON Layer with Interactive Styling
import { GeoJSON } from "react-leaflet";
import type { Feature } from "geojson";
function ChoroplethLayer({ data }: { data: GeoJSON.FeatureCollection }) {
function getColor(value: number): string {
if (value > 1000) return "#800026";
if (value > 500) return "#BD0026";
if (value > 200) return "#E31A1C";
if (value > 100) return "#FC4E2A";
return "#FED976";
}
function style(feature: Feature | undefined) {
const value = feature?.properties?.density ?? 0;
return {
fillColor: getColor(value),
weight: 2,
opacity: 1,
color: "white",
dashArray: "3",
fillOpacity: 0.7,
};
}
function onEachFeature(feature: Feature, layer: L.Layer) {
layer.on({
mouseover: (e: L.LeafletMouseEvent) => {
const target = e.target as L.Path;
target.setStyle({ weight: 5, color: "#666", dashArray: "", fillOpacity: 0.9 });
target.bringToFront();
},
mouseout: (e: L.LeafletMouseEvent) => {
const target = e.target as L.Path;
target.setStyle(style(feature));
},
click: () => {
alert(`${feature.properties?.name}: ${feature.properties?.density}`);
},
});
}
return <GeoJSON data={data} style={style} onEachFeature={onEachFeature} />;
}
Map Controls and Programmatic Navigation
import { useMap } from "react-leaflet";
function FlyToLocation({ position }: { position: [number, number] }) {
const map = useMap();
useEffect(() => {
map.flyTo(position, 14, { duration: 1.5 });
}, [map, position]);
return null;
}
function FitBoundsToMarkers({ positions }: { positions: [number, number][] }) {
const map = useMap();
useEffect(() => {
if (positions.length === 0) return;
const bounds = L.latLngBounds(positions);
map.fitBounds(bounds, { padding: [50, 50] });
}, [map, positions]);
return null;
}
Marker Clustering
// npm install react-leaflet-cluster
import MarkerClusterGroup from "react-leaflet-cluster";
function ClusteredMap({ points }: { points: Array<{ lat: number; lng: number; label: string }> }) {
return (
<MapContainer center={[51.505, -0.09]} zoom={10} style={{ height: "100vh", width: "100%" }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<MarkerClusterGroup
chunkedLoading
maxClusterRadius={60}
spiderfyOnMaxZoom
showCoverageOnHover={false}
>
{points.map((point, i) => (
<Marker key={i} position={[point.lat, point.lng]}>
<Popup>{point.label}</Popup>
</Marker>
))}
</MarkerClusterGroup>
</MapContainer>
);
}
Layer Control for Multiple Tile Providers
import { LayersControl, TileLayer } from "react-leaflet";
function MultiLayerMap() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: "100vh", width: "100%" }}>
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="OpenStreetMap">
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Satellite">
<TileLayer
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
attribution="Tiles © Esri"
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Topo">
<TileLayer
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
attribution="© OpenTopoMap"
/>
</LayersControl.BaseLayer>
</LayersControl>
</MapContainer>
);
}
Drawing Shapes with Leaflet.draw
// npm install leaflet-draw @types/leaflet-draw
import "leaflet-draw/dist/leaflet.draw.css";
import { FeatureGroup } from "react-leaflet";
import { EditControl } from "react-leaflet-draw";
function DrawableMap({ onShapeCreated }: { onShapeCreated: (geoJson: GeoJSON.Feature) => void }) {
const handleCreated = (e: L.DrawEvents.Created) => {
const layer = e.layer as L.Polygon | L.Circle | L.Rectangle;
const geoJson = (layer as L.Polygon).toGeoJSON();
onShapeCreated(geoJson);
};
return (
<FeatureGroup>
<EditControl
position="topright"
onCreated={handleCreated}
draw={{
rectangle: true,
polygon: true,
circle: false, // circle not natively GeoJSON-compatible
polyline: false,
marker: false,
circlemarker: false,
}}
/>
</FeatureGroup>
);
}
Best Practices
- Fix marker icons early: Bundlers (Webpack, Vite) break Leaflet's default icon paths. Apply the
mergeOptionsfix at app initialization, not inside components. - Use
whenReadyoruseMapfor post-init logic: Never access the map instance before it finishes initializing. TheuseMaphook guarantees the instance is ready. - Key GeoJSON components for re-rendering:
react-leaflet's<GeoJSON>does not update when data changes. Pass a uniquekeyprop to force remount when the data object changes. - Respect tile provider terms: OpenStreetMap's tile servers have a usage policy. For production traffic, use a commercial provider or host your own tile server.
- Debounce viewport-dependent data fetches: Listen to
moveend(notmove) and debounce to avoid overwhelming your API on every pan frame. - Set
maxBoundsfor constrained maps: If your app only covers a specific region, setmaxBoundson theMapContainerto prevent users from scrolling to irrelevant areas.
Anti-Patterns
- Including Leaflet CSS only in the component file: The CSS must be loaded globally or at the layout level. Missing CSS causes tiles to render in a broken grid pattern.
- Creating L.map() manually in React: Never call
L.map()yourself alongsidereact-leaflet. Use<MapContainer>exclusively -- it owns the Leaflet instance lifecycle. - Adding thousands of individual markers without clustering: Leaflet's DOM-based rendering slows dramatically past ~1,000 markers. Always use
MarkerClusterGroupor switch to Canvas/SVG renderers for large datasets. - Using setView inside render without guards: Calling
map.setView()on every render creates an infinite loop if the view change triggers a re-render. Gate navigation calls behind a change check. - Ignoring the
keyprop on GeoJSON: Without a fresh key, the GeoJSON layer keeps stale data. This is the most common bug in react-leaflet apps. - Loading heavy plugins unconditionally: Plugins like
leaflet-drawandleaflet.heatadd significant bundle weight. Dynamic-import them only on pages that use drawing or heatmap features.
Install this skill directly: skilldb add maps-geolocation-services-skills
Related Skills
Google Maps Platform
"Google Maps Platform: Maps JavaScript API, Places, Geocoding, Directions, Street View, React (@vis.gl/react-google-maps)"
HERE Maps
HERE Maps Platform: Maps API for JavaScript, geocoding, routing, isoline routing, fleet telematics, and platform data services
Mapbox
"Mapbox: interactive maps, GL JS, geocoding, directions, markers, layers, 3D terrain, React (react-map-gl)"
OpenLayers
OpenLayers: high-performance open-source map library with vector tiles, projections, OGC services (WMS/WFS), drawing interactions, and GeoJSON/KML support
OpenStreetMap & Nominatim
OpenStreetMap tile usage and Nominatim geocoding API: forward/reverse geocoding, tile servers, Overpass API queries, and attribution requirements
Radar
"Radar: geofencing, geocoding, trip tracking, address validation, maps, fraud detection, JavaScript SDK"