Add All Store Locations to a React Leaflet Map with Location Data From GraphCMS

Colby Fayock
InstructorColby Fayock

Share this video with your friends

Send Tweet
Published a year ago
Updated 10 months ago

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.

Instructor: [0:00] Now that we have our store's page and we have our store locations showing on that page, we want to be able to add a map so that people can easily navigate and see what's close to their physical location. To do that, we're going to use a Leaflet.

[0:13] In particular, we're going to use React Leaflet, where it's going to allow us to easily add a map into our store. To get started, we want to first install the dependencies that we need. That's going to include Leaflet as we already have React. That's going to include React Leaflet.

[0:27] In my terminal, I'm going to run yarn addLeaflet and React Leaflet. That's going to go ahead and install the dependencies. In our store's page, we already have a location for this map which is going to be underneath those locations that we did in the previous lesson.

[0:42] While we can probably get started by just adding the code inside of this container, we're instead going to take the route of creating a map component. As there's going to be configuration that we're going to need, which is going to be useful anywhere we would want to use a map.

[0:54] Then to get started, inside of my components' directory, we're going to follow along with the existing pattern where we have the button.js, the index.js, which reference that button.js. The reason we use an index.js with the button, is to make searching a little bit easier and how we import the file.

[1:11] Then we have a module, the SCSS file that's going to relate to that specific component. I'm going to create a new directory inside our components, and I'm going to call that map. Then I'm going to create a file called map.js. I'm going to also create another file and I'm going to call that index.js.

[1:28] Finally, I'm going to create my map.module.scss. What we want to do, is inside of our index.js file, similar to what we're doing in the button and any of the other components, is export the default value from that component. We're basically passing it right through.

[1:45] I'm going to say, export my default destructured from my map. Now, I'm going to create my new constant called map and set that equal to a new function. We're all ultimately return that map. I'm also going to export my default map.

[2:01] Then let's start importing all of our React Leaflet components. Then at the top of my map component, I'm going to first import those. Then I'm going to head back to the React Leaflet home page where I'm going to also start off with this existing demo.

[2:14] I'm going to go ahead and copy that MapContainer right into my return statement. I might as well also copy this position and paste it in at the top to make sure we get that same position. Then, finally, let's import this into our stores page.

[2:29] At the top of stores, I'm going to duplicate one of these lines and I'm going to say, I want to import my map from components' map. Then let's replace this map with our new map component.

[2:40] Now, if you head back to your application, we're going to hit our first roadblock here where the issue is Leaflet relies on the window in order to work, and they don't check that the window exist before trying to actually do anything. This is an issue for us because Next.js actually runs on node sometimes, and when it runs on node, it's going to create that error where it can't access the window.

[3:03] To fix this, we're going to use Next.js Dynamic Imports, which is going to allow us to import it whenever we load the page. To do this, we're going to first import dynamic from next/dynamic. We're going to actually do this inside of our map index.js file.

[3:19] I'm going to first import that at the top of that file. We're going to handle this just like we have it here where we're going to create our dynamic component. I'm going to paste that in, and I'm going to start replacing things saying that I want to create a new constant of map. I want that to be a dynamic import where we actually import that .map file.

[3:38] That means that we're going to need to change this export, and we're going to say we want to export a default map. Now, there's one last thing we want to add and that's an options object to dynamic where we want to say that we want SSR or server-side rendering to be false. That way, it doesn't try to render it inside of node.

[3:56] Now, when the page reloads, while it looks a little bit wonky, we see that we're getting a map. Now, the next issue is we need to apply some default styles in order for it to work. When we're doing so with React Leaflet, we're not getting the default Leaflet styles to make sure that the map is setting up properly.

[4:14] If we go to our terminal, and we look inside the node_modules/leaflet/dist directory, we're going to see a bunch of files where we don't need to worry about most of them as React Leaflet is going to handle the most of this, but what we want to do is we want to import this CSS file. To do that with my map component, I'm going to say I want to import leaflet/dist/leaflet.css.

[4:38] Now, when our page reloads, we actually don't even have a map anymore. If we start to inspect inside of the DOM, and we start to look through these nodes, we're going to see that the leaflet container is zero. That's because it doesn't automatically expand to fill its container. We have to help it out with that.

[4:54] First of all, I want to set the width of my map to be 100 percent. On my MapContainer, I'm going to create a new class name, and I'm going to say I want that to be styles.map, where if I head over to map.module.scss, I'm going to define that map style and simply say width is 100 percent.

[5:11] Now, those styles aren't going to automatically import themselves, so we need to also import our styles from our map.module.scss. At this point, it's still not filling its container, so we need to take another step.

[5:25] This step needs to be specific to the location of the map, where we have a specific idea of how we want it to fit on this page, but we don't want to assume that for every page. We're not going to do the same thing here as we are in other place.

[5:39] Now, because of that, the way that we're going to handle this, is we're going to allow this map component to take in a class name. Where on my stores page, I can say, I want this to have a class name of styles.map, which I already have defined and we'll see in a second.

[5:54] This way, they're not going to collide because the way the Next.js is going to handle CSS modules, is it'll be scoped to this specific page. I can still define styles specific to stores versus map, where now I can say that as a prop, I want to take in class name where I'm going to create a new let variable and say, map class name is equal to my styles.map.

[6:20] If I have a class name, I can say that I want my map class name to equal my map class name, and then my additional class name. Finally, I can replace that class name prop with map class name. Before we see what's happening, let's look at what's on the styles of map that I already predefined inside of this starter template.

[6:43] Once we look over there, we can see that I have that map. All I'm doing is setting a width of 100 percent and a height of 100 percent. If we look above that, our stores MapContainer, which if we look inside of the DOM, is going to be the parent.

[6:55] We're defining an aspect ratio, meaning we're setting a responsive boundary saying that we always want it to be in that particular ratio. Then our map, we want it to always take up the full width and the full height of that area. When the page reloads, we can see it's doing exactly that.

[7:13] If you want to tweak it, you can tweak that aspect ratio, but it's even going to be responsive for us. If we open up our developer tools and we start to shrink the area, we can see that it's going to scale to that exact ratio. Before we move on, we should notice that we also have another issue.

[7:29] We're supposed to be showing a marker on the screen. If we look at the map, we're centered to that location, but we're not showing that marker. If we look at the web console, we can see that Next.js is trying to load those images, but it's not finding it in the right location.

[7:44] We're going to have to manually grab those images and assign them to be loaded for our map. Back in our terminal, similar to what we did before with the CSS, we can see all the images that are available inside of the leaflet/dist/images directory.

[7:59] Particularly, we want to import all these markers, so I'm going to start off by importing my icon marker two times from leaflet/dist/images, and then I'm going to paste in marker icon two times.

[8:12] I'm going to then clone that two times, because I know I'm going to need those to other files where I'm going to remove the two X from the one, and I'm going to say icon marker shadow for my third one. I'm also going to do the same exact thing and make sure that I update the images that are getting referenced to what the images should be.

[8:29] Next, I want to tell Leaflet as soon as the page loads to load those specific images. To do this, we're going to use the useEffect Hooks. I'm going to import useEffect from React at the top of my file. Then I'm going to scroll down inside of my component right before the return statement.

[8:47] I'm going to say useEffect, where inside, I'm going to pass in first of all, a blank function. Then we're going to pass in an empty array as a second argument, so that it only runs once. The particular code that we're going to be using is From a GitHub issue that I found in the past.

[9:02] Where we can see here what they're going to do, is they're going to first delete the function that's going to get that URL. Then we're going to define all the different URLs for that particular marker and the shadow. I'm going to go ahead and copy that snippet and I'm going to paste it into my useEffect.

[9:19] Where then I want to update each of these instances to what I imported above. I'm going to first grab my icon marker.2x. Where I'm going to say, I want to import that and I want to import the .source of that value. Then I'm going to do the same thing for the next two, where we have our icon marker as is.

[9:38] Then finally, the icon marker shadow, where we also want to reference that at icon marker shadow. Once the page reloads, we can see our beautiful marker and we can even click it because that's part of the functionality where we can see that pop up.

[9:52] Now, let's add one of these markers for every single one of our locations. Looking at our map, we don't want to load these same markers for every single location. Instead, we want to be able to reference these markers from wherever we're trying to load it.

[10:06] Now, this is tricky because we don't want to create an API so specific that we're actually recreating the entire React Leaflet API.

[10:15] We also need to pay attention to how we're importing this map as we're doing so dynamically. Instead of importing these markers here, I'm going to say that I want to import anything inside of this map over on my stores page.

[10:28] I'm going to first open this up and I'm going to say inside of my map, I want to load those components. As you might expect, first of all, we don't have those components in this page, but second of all, we can't import each and one of these because again, the dynamic imports.

[10:44] To solve this, we're going to use the MapConsumer function where if we look inside of this example here, we're going to create this wrapper component of MapConsumer where we're going to pass as a function, the entire map.

[10:57] With it, we're going to additionally pass in Leaflet components. That way, in the actual user of the component, we're going to be able to have access to all that information and do whatever we want. I'm going to first copy this example as is.

[11:12] Inside of my map component, I'm going to go ahead and paste that in, but we also need to import this MapConsumer. I'm going to tap that onto my React Leaflet import. Next, I want all of my children of this component to have access to that.

[11:25] To do that, we can run our children prop as a function where typically, we might add our children simply like this, which is going to render anything nested inside of that component, but we can pass data to that child, so that we can have access to it in the user of that component.

[11:44] I'm going to get rid of this default return statement and I'm going to simply pass in that in location of children. First of all, let's add our children as an actual prop, because we need to be able to access it in order to use it.

[11:57] Again, trying to solve the problem of loading this dynamically, we want to be able to pass our children, anything inside of React Leaflet, so that it has access to it, even in that dynamic way. The way we're going to handle this, is we're going to pass in React Leaflet, that way we can take anything we want for that.

[12:17] To do that, instead of importing each of these individual components, I'm going to say, I want to import star which is everything as React Leaflet from React Leaflet. We can't simply get rid of this import statement. We still need to use that MapConsumer and the MapContainer.

[12:34] I'm going to instead take that destructuring statement. I'm going to move it to the bottom of the imports and say that I want to destructure those from React Leaflet to make sure that we do still have those available. This means our children of this component is going to have access to everything inside of React Leaflet.

[12:53] Let's also pass in the map as a second argument as we'll need that later. Back on our stores page, we want to be able to access all that information. To do that, we want to return a function inside of this component, just as we did inside of the component itself.

[13:07] We're going to say inside here, we want to pass in a function, where, if we remember that first argument is going to be React Leaflet, where the second argument is going to be map. We ultimately want to return a component.

[13:19] Next, I'm going to create a fragment because I'm going to move up this marker and the tile layer right inside of that fragment. Instead of importing the entire React Leaflet, I'm going to only import these components that I'm going to be using.

[13:33] I'm going to first destructure this, I'm going to say tile layer and marker and my pop up. Finally, inside of this configuration, we can see that our marker is also referencing a position. We need to grab that from our original map instance.

[13:49] I'm going to come over to my stores. I'm going to paste this right at the top of this return statement, since it's going to be temporary anyways. We can see like magic, our map is still loading. Before we move onto the locations to further clean things up, we can see that we're additionally passing our MapContainer a few more things.

[14:06] That's additionally, the center position, which we don't want to be referencing inside of the map component, a zoom level, and a scroll-wheel zoom. While these aren't super important themselves, what we want to make sure we do, is be allowed to pass those in from the reference of the component.

[14:23] The first thing I'm going to do is inside of my props, I'm going to destructure the rest of everything inside of this object. Meaning, anything else I pass in, in addition to children and class name, it's going to be contained inside of REST.

[14:37] Then I'm going to take that REST and go back to my MapContainer and I'm going to spread everything out so that everything inside of that applies to the MapContainer. Then I'm going to grab all these props and I'm going to cut them out, head over to my stores page and simply add them onto the reference of my map component.

[14:55] Now, because we're referencing that position, we need to move our position up a little bit. I'm going to add that to the top of the file, but soon enough, we're not going to need that anyways. Yet again, we can see that it's still loading.

[15:09] Let's add a marker for each location. Still on the stores page in our code, we want to make sure that we're adding a new marker for every single location. We can take inspiration for this by the way that we were handling that list of locations originally in the sidebar of this page.

[15:24] I'm going to first copy that map statement and head back down to my map. I'm going to first paste than in. I'm going to finish out what that's going to return, which is first of all, going to return an empty item. Let's move up that marker inside where we're going to dynamically create this position.

[15:42] In order to create this position dynamically, we need the latitude and longitude from the location. I'm going to create a new constant called latitude destructured and longitude. Where I'm going to say, I want to destructure that from.

[15:54] If we remember, it's not the top-level location, it's going to be location.location. I'm going to say, I want it to be from location.location. Then I'm going to be able to take this latitude and longitude, and instead of this position variable, I'm going to pass in an array, which is going to be our coordinates of latitude, longitude.

[16:15] On top of that, we can also replace the pop up. Let's say we have a paragraph tag and let's add location.name. Then we're going to also add another paragraph and let's say, location.address. If we look at the map, oh, no, we don't have have any markers, but that's because we don't have any locations in the city of London.

[16:34] To make this easier to see. First of all, let's set this center of the map to a new set of coordinates of 00, which is simply the center of the world. Let's set a high zoom such as maybe four, that way it's going to be expanded out on the world until we can do this dynamically, which we'll learn in the next lesson.

[16:50] That means we can also get rid of that position like I was talking about earlier from the top. Once the map refreshes, we can see that we are in the center of the world 00, but it is a little bit too far zoomed in. We can click the minus a few times where we're going to start to see our locations wherever you added them.

[17:06] Where for instance, I added them in the United States inside of San Francisco, New York, and Philadelphia. If I start to zoom in, we're going to see those locations. If I click one of them, I see Philadelphia and the address.

[17:19] Before we move on as a quick tweak, we can definitely update that zoom to whatever we want it to be by default. Where with that lower zoom level, we definitely get a wider view of the world. Now, we have our list of locations and we have our locations on our map.

[17:32] In review, we wanted to add all of our locations on a map. To do this, we used React Leaflet, which is a wrapper around the Leaflet API.

[17:41] To make sure that we can create this map and use it anywhere, we created a map component, where to alleviate some issues within React and server-side rendering and being able to load this map inside of our application, we first had to manually import some of our marker images.

[17:57] We also created a dynamic import where we were able to create our map as an actual component. That allowed us to avoid any issues, where our window wasn't available during that compilation process.

[18:08] Once we had our component setup, we were able to pass in all of our React Leaflet components so that we were able to dynamically render all of our different store locations by mapping through each of those locations.

[18:19] Just like we did before, with our other locations, creating a new marker for each one, which allows us to see each of those locations right on the map.

Luis Ruiz
Luis Ruiz
~ 10 months ago

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

Bruno Ribeiro
Bruno Ribeiro
~ a week ago

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:

  • Asset fixing was moved to useFixLeafletAssets;
  • Props are now:
    • className: string;
    • locations: Array<{ latlong: [string, string], popupContent: ReactNode }>;
    • zoomedLatlong: [string, string];
// 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='&copy; <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;