Because I was unable to find a full example that solved all of my issues, I'm posting my code. Working in January 2024 with the latest versions of leaflet
, react-leaflet
, react
, and next.js
. This example should allow the client to load markers (from the CDN), but also refresh the page even if it's being delivered from server-side contexts like next.js
.
import React, { useEffect, useState } from 'react';
export const Map = ({ mapMarkers, height }) => {
const [Leaflet, setLeaflet] = useState(null);
const [MapContainer, setMapContainer] = useState(null);
const [TileLayer, setTileLayer] = useState(null);
const [Marker, setMarker] = useState(null);
const [Popup, setPopup] = useState(null);
useEffect(() => {
import('react-leaflet').then((mod) => setMapContainer(mod.MapContainer));
import('react-leaflet').then((mod) => setTileLayer(mod.TileLayer));
import('react-leaflet').then((mod) => setMarker(mod.Marker));
import('react-leaflet').then((mod) => setPopup(mod.Popup));
import('leaflet').then((mod) => setLeaflet(mod));
}, []); // Run imports just once on mount
const [customIcon, setCustomIcon] = useState(null);
const [initialBounds, setInitialBounds] = useState(null);
useEffect(() => {
if (Leaflet && mapMarkers.length > 0) {
// Calculate the bounds based on marker positions
const markerBounds = mapMarkers.map(({ position }) => position);
const bounds = Leaflet.latLngBounds(markerBounds);
// Pad the bounds to add some padding to the viewport
const paddedBounds = bounds.pad(1); // Adjust the padding factor as needed
// Set initial bounds for the MapContainer
setInitialBounds(paddedBounds);
// Initialize Leaflet once the component is mounted on the client side, and provide icons.
const icon = new Leaflet.Icon({
iconUrl: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/images/marker-icon.png',
iconSize: [22, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
setCustomIcon(icon);
}
}, [Leaflet, mapMarkers]); // Run the effect whenever Leaflet or mapMarkers are updated
return (
MapContainer && initialBounds && (
<MapContainer
bounds={initialBounds}
style={{ width: '100%', height: height || '400px' }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{customIcon &&
mapMarkers.map(({ position, popupContent }, index) => (
<Marker key={index} position={position} icon={customIcon}>
<Popup>{popupContent}</Popup>
</Marker>
))}
</MapContainer>
)
);
};
The component employs useEffect
hooks to avoid Server Side Rendering. Because of the dependency on Leaflet.js
, the map cannot be rendered server-side. Leaflet was built, and continues to be developed, for the context of rendering maps in standard browser environments. In such contexts, it is expected that Leaflet will have access to the Window object. This is categorically untrue in server side contexts, and so this component falls back on Client Side Rendering, using a CDN to deliver static assets (as Leaflet expects).
I have not taken the time to add types; please feel free to make that effort if you're so moved!