Displaying a list of physical locations is helpful, but showing them on a map, giving your potential customers the ability to see how close each store really is to their location is even better.
GraphCMS supports adding coordinates as a field to our data models, where we can dynamically pull those coordinates in and add them to a map.
We'll walk through how we can loop through our store locations and add each one to a new React Leaflet map. We'll also learn about some pitfalls of using React Leaflet in an app framework like Next.js and how we can solve it with dynamic imports and manual image imports.
I also have a whole course on how to Build Maps using React Leaflet if you want a further dive on the topic.
Hi, just for the record, if you're doing this with react-leaflet v4 here is the component you need to use for the Map. I did this in TS so it looks different and I hope it's understandable.
import L from 'leaflet';
import iconMarker2x from 'leaflet/dist/images/marker-icon-2x.png';
import iconMarker from 'leaflet/dist/images/marker-icon.png';
import iconMarkerShadow from 'leaflet/dist/images/marker-shadow.png';
import 'leaflet/dist/leaflet.css';
import { useEffect, useRef } from 'react';
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet';
import styles from './Map.module.scss';
const Map = ({
className,
children,
...rest
}: L.MapOptions & {
className?: string;
children?: (
ReactLeaflet: {
Marker: typeof Marker;
Popup: typeof Popup;
TileLayer: typeof TileLayer;
},
map: L.Map
) => React.ReactNode;
}) => {
const mapRef = useRef(null);
let mapClassName = styles.map;
if (className) {
mapClassName = `${mapClassName} ${className}`;
}
useEffect(() => {
delete L.Icon.Default.prototype['_getIconUrl'];
L.Icon.Default.mergeOptions({
iconRetinaUrl: iconMarker2x.src,
iconUrl: iconMarker.src,
shadowUrl: iconMarkerShadow.src,
});
}, []);
return (
<MapContainer className={mapClassName} {...rest} ref={mapRef}>
{children ? children({ Marker, Popup, TileLayer }, mapRef.current) : null}
</MapContainer>
);
};
export default Map;
The store part remains the same
react-leaflet
4.x exports a useMap()
hook that makes it possible to implement the "View on map" mechanic without making the page component responsible for the map template.
This is my code. Note that:
useFixLeafletAssets
;// Map.jsx
import { useEffect } from "react";
import { MapContainer, TileLayer, Marker, Popup, useMap } from "react-leaflet";
import center from '@turf/center';
import { points } from '@turf/helpers';
import useFixLeafletAssets from "./useFixLeafletAssets";
import styles from './Map.module.scss';
import "leaflet/dist/leaflet.css";
function LocationZoomer({ latlong }) {
const map = useMap();
useEffect(() => {
if (latlong) {
map.setView(latlong, 16);
}
}, [map, latlong]);
return null;
}
function Map({ locations = [], className = '', zoomedLatlong = null }) {
useFixLeafletAssets();
const features = points(locations.map(location => location.latlong));
const initialMapCenter = center(features)?.geometry.coordinates || [0,0];
return (
<MapContainer className={`${styles.map} ${className}`} center={initialMapCenter} zoom={4} scrollWheelZoom>
<LocationZoomer latlong={zoomedLatlong} />
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{locations.map(({ latlong, popupContent }) => (
<Marker key={latlong.join('-')} position={latlong}>
<Popup>
{popupContent}
</Popup>
</Marker>
))}
</MapContainer>
);
}
export default Map;