How to Build an Interactive Map with React Leaflet and Strapi
Tags: nextjs, strapi, interactivemaps, reactleaflet
Published: 2024-05-17 10:05:46

![How to Build an Interactive Map Using React Leaflet and Strapi](https://hackmd.io/_uploads/BJhIRhX-R.jpg)

## Introduction

Interactive maps are an essential part of many web and mobile applications. They provide users with a visual representation of geographical data enabling them to interact with it in meaningful ways. They transcend the limitations of static maps by offering dynamic features that empower users to explore, analyze, and interact with geographical data in real time. 

In this tutorial, you'll learn how to build an interactive map application using React Leaflet and Strapi. By the end of this tutorial, you'll learn how to create a map that displays location markers and enables searching for locations. Additionally, you'll discover how to enable the dragging of markers, display location information including images, and allow the drawing of polygons.
## Prerequisites
To comfortably follow along, you'll need to have:
* [NodeJS](https://nodejs.org/en/download/) installed in your computer
* Basic understanding of [JavaScript](https://developer.mozilla.org/en-US/docs/Web/javascript)
## Creating and Managing Geographic Data in Strapi

### Install Strapi
Begin by creating the geographic data necessary for pinpointing locations on the map and presenting the relevant information. To achieve this run the following command on the terminal:
```bash
npx create-strapi-app@latest my-project
```
The command will initialize a new [Strapi](https://strapi.io/) project named **my-project**. When the initialization is complete, on your terminal, navigate to the `my-project` directory and run the following command:

```bash
npm run develop
```
The command will start a development server for your `Strapi` project. Open the default **Strapi Admin URL** http://localhost:1337/admin to access the strapi admin registration panel.

![admin-registration](https://hackmd.io/_uploads/H1koOtMbR.jpg)

Register yourself to access the admin dashboard.

![dashboard](https://hackmd.io/_uploads/BJuwOwbZC.jpg)

### Create Collection Type
Click on **Content-Type Builder** and create a new collection type named `Location` and click **Continue**.

![collection-naming](https://hackmd.io/_uploads/BJlndDWWR.jpg)

Create five fields named: `name`, `description`, `longitude`, `latitude`, and finally `photo`. The `name` and `description` should be of type **text**, `longitude` and `latitude` should be of type **number**, and should hold  **float** number format. And the `photo` should be a **Media** field of type **single**.


> **Note**: Ensure to click **save** when you are done creating these fields.


![fields](https://hackmd.io/_uploads/S1y_3Pb-C.jpg)

### Create Entries
Proceed to the **Content Manager** and create a new entry. Fill all the fields  with the data of the location you want to pinpoint and describe on the map. For the purpose of this tutorial, we will input the data of several capital cities around the world. You can obtain the latitude and longitude coordinates from Google Maps.


![create-an-entry](https://hackmd.io/_uploads/HkC5yO--0.jpg)

Enter as many entries as you wish, after each entry, click **Save** and **Publish** respectively.

### Enable API Public Access
You now have your data stored in the **Strapi** database. To expose this data to be used by an application or a program, head to ***Settings > Users & Permissions Plugin > Roles > Public***.

![public-path](https://hackmd.io/_uploads/SJCNZdbZR.jpg)

Then click on the location and turn on `find` and `findOne`.

![find-findone](https://hackmd.io/_uploads/Hk1R-Ob-R.jpg)

Proceed to the location endpoint at `http://localhost:1337/api/locations?populate=photo` to see the structure of your data. The `?populate=photo` parameter at the end of the URL instructs Strapi to include the associated photo data for each location in the response. 

Here is a sample data from the endpoint:

![data-endpoint-example](https://hackmd.io/_uploads/B1OQmdbWC.jpg)

This is the data you will later use to create the interactive map. You now need an app that will use the data to showcase an interactive map. 
## Setting up the App's Development Environment

### Install Next.js
Navigate to the directory where you want to host your project using your preferred IDE. Then, open the terminal and execute the following command:
```bash
npx create-next-app@latest
```
Choose the following options:

![options](https://hackmd.io/_uploads/rkuOOu-Z0.jpg)

The command will set up a new `Next.js` project named `map-app` using the latest available version.

### Install Other Dependencies
Then install the required dependencies using this command:
```bash
npm install react-leaflet react-leaflet-draw axios
```
Here is what each library will do:
* **[`react-leaflet`](https://react-leaflet.js.org/)**: This library provides `React` components for building interactive maps using the `Leaflet` JavaScript library. 
* **[`react-leaflet-draw`](https://www.npmjs.com/package/react-leaflet-draw)**: This library extends `react-leaflet` by providing components for adding drawing functionalities to the map.
* **[`axios`](https://axios-http.com/docs/intro)**: This library will make HTTP requests to fetch data from the Strapi API containing the geographical data.

`react-Leaflet` provides bindings between [React](https://react.dev/) and [Leaflet](https://leafletjs.com/), leveraging Leaflet to abstract its layers as React components. It does not replace Leaflet but rather works with it. Both [`leaflet`](https://www.npmjs.com/package/leaflet) and [`leaflet-draw`](https://www.npmjs.com/package/leaflet-draw) are installed as peer dependencies of `react-Leaflet` and `react-leaflet-draw` respectively.

After the dependencies are installed, you are free to start coding.
## Fetching Location Data from Strapi
On your `map-app` project, inside the `api` folder of the `pages` folder, create a new file named `locations.js`. This will be the file containing the code responsible for fetching location data from Strapi.
```javascript
// pages/api/locations.js
import axios from "axios";

export default async (req, res) => {
  try {
    const response = await axios.get(
      "http://localhost:1337/api/locations?populate=photo",
    );
    res.status(200).json(response.data);
  } catch (error) {
    console.error("Error fetching data:", error);
    if (error.response) {
      res
        .status(error.response.status)
        .json({ error: error.response.data.message });
    } else {
      res.status(500).json({ error: "Error fetching data" });
    }
  }
};

```
The code utilizes the Axios library for HTTP requests. The route sends a `GET` request to the Strapi endpoint. Upon successful retrieval of data, the route responds with a status code of `200` and sends the fetched data as `JSON`. Error handling logs any encountered errors and provides appropriate error responses to client requests. This includes both server-generated and network-related errors. 

## Creating the Interactive Map
After your app has fetched data from Strapi, you are ready to create the interactive map. Proceed to the `map-app` project root directory and create a folder named `components`. Then create a component named `Map.js` inside this folder. 

This file will contain the code responsible for creating and rendering the interactive map. The code has been divided into smaller sub-sections for easier understanding. 
### Importing the Required Libraries
Open the `Map.js` file and start by importing the required libraries and initializing the state variables:
```javascript
// components/Map.js
import React, { useState, useEffect, useRef } from "react";
import {
  MapContainer,
  TileLayer,
  Marker,
  Popup,
  Polygon,
  FeatureGroup,
  useMap,
} from "react-leaflet";
import { EditControl } from "react-leaflet-draw";
import "leaflet/dist/leaflet.css";
import "leaflet-draw/dist/leaflet.draw.css";
import L from "leaflet";

const Map = () => {
  const [locations, setLocations] = useState([]);
  const [filteredLocations, setFilteredLocations] = useState([]);
  const [selectedLocation, setSelectedLocation] = useState(null);
  const [searchQuery, setSearchQuery] = useState("");
  const [polygonPoints, setPolygonPoints] = useState([]);
  const mapRef = useRef(null);


```
Here is a breakdown of what each main component does:

 * `MapContainer`: This is the main container for your map.
    * `TileLayer`: It defines the background map tiles.
    * `Marker`: It represents location markers on the map.
    * `Popup`: This is the information window displayed when clicking a marker.
    * `Polygon`: Represents a polygon drawn on the map.
    * `FeatureGroup`: Groups your map features together for easier management.
    * `useMap`: This is a hook used within the component to access the map instance.
* `EditControl` from `react-leaflet-draw`: It enables drawing functionalities on the map.
* Leaflet CSS (`leaflet/dist/leaflet.css`) and Leaflet Draw CSS (`leaflet-draw/dist/leaflet.draw.css`): They provide styles for the map and drawing tools.

These components will be crucial when adding functionalities to your map.

### Fetching Location Data From the Next.js API
Inside the `Map.js` file, Create a `useEffect` hook responsible for fetching location data from the server when the `Map` component mounts. For each location, it will extract the URL of the photo associated with that location (if available) and construct the `photoUrl` property. 
```javascript
// components/Map.js
useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch("/api/locations");
      const data = await response.json();
      const locationsWithPhotoUrl = data.data.map((location) => ({
        ...location,
        photoUrl: `http://localhost:1337${location.attributes.photo?.data[0]?.attributes?.url}`,
      }));
      setLocations(locationsWithPhotoUrl);
      setFilteredLocations(locationsWithPhotoUrl);
    } catch (error) {
      console.error("Error fetching data:", error);
    }
  };
  fetchData();
}, []);

```
In the code above, we need to construct the `photoUrl` property because the data obtained from strapi includes the relative path. But for the browser to display the image it needs the complete URL else the image won't be visible.
### Enabling Marker Clicking and Adding Search Functionality
The map needs to respond to user interactions, such as clicking on markers or searching for locations. To achieve this define the relevant event handlers:
```javascript
// components/Map.js
const customIcon = L.icon({
  iconUrl: "/custom-marker.png",
  iconSize: [30, 30],
  iconAnchor: [15, 30],
});

const handleMarkerClick = (location) => {
  setSelectedLocation(location);
  mapRef.current.flyTo(
    [location.attributes.latitude, location.attributes.longitude],
    14, // Zoom level
  );
};


```
The code above defines a custom marker icon and creates a `handleMarkerClick` function that handles user clicks on markers. When a marker is clicked, it updates the state variable `selectedLocation` with the details of the clicked location. This enables the map to display the information about that location. It utilizes the Leaflet `flyTo` method to smoothly animate the map view to the coordinates of the clicked location.

The image below is what we used as the custom icon. Make sure to download it and put it inside the `public` folder with the name `custom-maker.png`.

> **NOTE**: you can use any image of your choice and replace with `custom-marker.png` in your code.


![custom-marker](https://hackmd.io/_uploads/r1_-Yz-MR.png)


```javascript
// components/Map.js
const handleSearch = () => {
  if (searchQuery.trim() === "") {
    setFilteredLocations(locations);
    setSelectedLocation(null);
  } else {
    const filtered = locations.filter((location) =>
      location.attributes.name
        .toLowerCase()
        .includes(searchQuery.toLowerCase()),
    );
    setFilteredLocations(filtered);
    if (filtered.length > 0) {
      setSelectedLocation(filtered[0]);
      mapRef.current.flyTo(
        [filtered[0].attributes.latitude, filtered[0].attributes.longitude],
        14, // Zoom level
      );
    } else {
      setSelectedLocation(null);
    }
  }
};

```
The `handleSearch` function filters the list of locations based on the user-entered search query, updating the `filteredLocations` state accordingly. If there are matching locations, it selects the first one from the filtered list and adjusts the map view to focus on its coordinates. If no matching locations are found, it clears the selected location, providing feedback to the user that no results were found. 
```javascript
// components/Map.js
const handleDragMarker = (event, location) => {
  const newLatLng = event.target.getLatLng();
  const updatedLocations = locations.map((loc) =>
    loc.id === location.id
      ? {
          ...loc,
          attributes: {
            ...loc.attributes,
            latitude: newLatLng.lat,
            longitude: newLatLng.lng,
          },
        }
      : loc,
  );
  setLocations(updatedLocations);
  setFilteredLocations(updatedLocations);
};

```
The `handleDragMarker` function updates the position of a marker on the map when it is dragged by the user. When a marker is dragged to a new location, this function captures the new latitude and longitude coordinates of the marker from the drag event. It then updates the locations state variable by mapping over the existing array of locations and modifying the coordinates of the dragged marker.
### Adding a Polygon Drawing and Editing Functionality
For the map to be interactive, allowing users to draw polygons and edit them. You need to handle map interactions.
```javascript
// components/Map.js
const _onCreate = (e) => {
  const { layerType, layer } = e;
  if (layerType === "polygon") {
    setPolygonPoints(layer.getLatLngs()[0]);
  }
};

const _onEdited = (e) => {
  const {
    layers: { _layers },
  } = e;
  Object.values(_layers).map(({ editing }) => {
    setPolygonPoints(editing.latlngs[0]);
  });
};

const _onDeleted = (e) => {
  const {
    layers: { _layers },
  } = e;
  Object.values(_layers).map(() => {
    setPolygonPoints([]);
  });
};

```
When a new polygon is drawn, its coordinates are extracted and used to update the `polygonPoints` state, reflecting the newly drawn polygon. When it is edited, the updated coordinates are captured and applied to the `polygonPoints` state, ensuring that changes to the polygon's shape are reflected. Lastly, when a polygon is deleted, the `polygonPoints` state is reset to an empty array, indicating the absence of any polygons on the map. 
### Creating the User Interface
The final step in creating the interactive map component is rendering the user interface elements:
```javascript
// components/Map.js
  return (
    <div>
      <input
        type="text"
        placeholder="Search locations"
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        style={{
          color: "black",
          border: "1px solid #ccc",
          borderRadius: "4px",
          padding: "8px",
        }}
      />
      <button
        onClick={handleSearch}
        style={{
          backgroundColor: "green",
          color: "white",
          border: "none",
          borderRadius: "4px",
          padding: "8px 16px",
          marginLeft: "8px",
        }}
      >
        Search
      </button>
      <MapContainer
        center={[0, 0]}
        zoom={2}
        style={{ height: "80vh" }}
        ref={mapRef}
      >
        <TileLayer
          attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <FeatureGroup>
          <EditControl
            position="topright"
            onCreated={_onCreate}
            onEdited={_onEdited}
            onDeleted={_onDeleted}
            draw={{
              rectangle: false,
              polyline: false,
              circle: false,
              circlemarker: false,
              marker: false,
            }}
          />
        </FeatureGroup>
        {Array.isArray(filteredLocations) &&
          filteredLocations.map((location) => (
            <Marker
              key={location.id}
              position={[
                location.attributes.latitude,
                location.attributes.longitude,
              ]}
              icon={customIcon}
              eventHandlers={{
                click: () => handleMarkerClick(location),
                dragend: (event) => handleDragMarker(event, location),
              }}
              draggable
            >
              <Popup>
                <div
                  style={{ width: "300px", height: "Auto", paddingTop: "10px" }}
                >
                  <h3>{location.attributes.name}</h3>
                  {selectedLocation?.id === location.id && (
                    <div>
                      <p>{location.attributes.description}</p>
                      {location.photoUrl && (
                        <img
                          src={location.photoUrl}
                          alt={location.attributes.name}
                          style={{ maxWidth: "100%", height: "auto" }}
                        />
                      )}
                    </div>
                  )}
                </div>
              </Popup>
            </Marker>
          ))}
        {polygonPoints.length > 0 && (
          <Polygon positions={polygonPoints} color="blue" fillOpacity={0.5} />
        )}
      </MapContainer>
    </div>
  );
};

export default Map;


```

The code above renders your map's user interface. It consists of several elements: a search input field, a button for filtering locations, a map container initialized with `Leaflet`, a tile layer for displaying map tiles from `OpenStreetMap`, and an `EditControl` component allowing users to draw and edit polygons on the map. For each filtered location, markers are rendered with custom icons.

## Rendering the Map Component on the Homepage
Proceed to the `index.js` file which is located under the `pages` directory and paste the following code:
```javascript
// pages/index.js
import React from "react";
import dynamic from "next/dynamic";

const Map = dynamic(() => import("../components/Map"), {
  ssr: false,
});

const Home = () => {
  return (
    <div>
      <h1>Interactive Map</h1>
      <Map />
    </div>
  );
};

export default Home;

```
The code renders the map component with all the interactive features you've implemented, such as displaying location markers, enabling search, allowing marker dragging, and drawing polygons.

Look at how the homepage user interface looks:

![user-interface](https://hackmd.io/_uploads/rkqEUs--A.jpg)

It shows all the capital cities whose data is stored in Strapi.

## Testing the Application
Below is a GIF showing all the features we have implemented:
<iframe src="https://giphy.com/embed/6RhLJZDucMdZNNb1Lf" width="750" height="450" frameBorder="0" class="giphy-embed" allowFullScreen></iframe><p><a href="https://giphy.com/gifs/6RhLJZDucMdZNNb1Lf">via GIPHY</a></p>




The GIF shows all the functionalities working seamlessly.

## Conclusion
In this tutorial, you have learned to set up Strapi, fetch data, build the interactive map, and integrate it into a Next.js app. This shows how integrating Strapi and React-Leaflet can produce visually powerful maps.

Go ahead and implement more custom functionalities on the map!
## Additional Resources
* The full source code is available in [this repository](https://github.com/FINCH285/Interactive-map-using-react-leaflet-and-strapi) and the Strapi backend [here](https://github.com/FINCH285/Map-App-Strapi-Backend).
* https://docs.strapi.io/dev-docs/backend-customization
* https://react-leaflet.js.org/