When trying to show store locations on a map, showing the entire world by default when you only have a few locations to show means more work for your customer, trying to narrow down and finding where they actually are. Instead, a better UX could be zooming into a specific portion of that map, centering it in the middle of all of your locations.
To do this, we can use geospatial analysis tools like Turf.js which allows us to provide a set of coordinates and calculate where that center is.
We'll walk through setting up Turf.js in a Next.js app and using it to calculate the center of all of our online stores. In order to allow someone to select the store and automatically zoom in, we'll also use the useEffect hook along with our React Leaflet map to change the view whenever someone clicks on a store.
Again me here commenting the solution when developing this with leaflet v4.
// Map/Map.tsx
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 } 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;
}) => React.ReactNode;
}) => {
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}>
{children ? children({ Marker, Popup, TileLayer }) : null}
</MapContainer>
);
};
export default Map;
// Map/MapEffect.tsx
import { useEffect } from 'react';
import { useMap } from 'react-leaflet';
type MapEffectProps = {
activeStore: string;
storeLocations: {
id: string;
name: string;
phoneNumber: string;
address: string;
location: {
latitude: number;
longitude: number;
};
}[];
};
const MapEffect = ({ activeStore, storeLocations }: MapEffectProps) => {
const map = useMap();
useEffect(() => {
if (!activeStore || !map) {
return;
}
const { location } = storeLocations.find(({ id }) => id === activeStore);
map.setView([location.latitude, location.longitude], 14);
}, [activeStore, storeLocations, map]);
return null;
};
export default MapEffect;
// Map/index.ts
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('./Map'), { ssr: false });
const MapEffect = dynamic(() => import('./MapEffect'), { ssr: false });
export default Map;
export { MapEffect };
Hey Luis, Awesome, thanks for share. Do you have it without typescript?