How to Build an Interactive Map with React Leaflet and Strapi
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 installed in your computer
- Basic understanding of 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:
npx create-strapi-app@latest my-project
The command will initialize a new Strapi project named my-project. When the initialization is complete, on your terminal, navigate to the my-project
directory and run the following command:
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.
Register yourself to access the admin dashboard.
Create Collection Type
Click on Content-Type Builder and create a new collection type named Location
and click Continue.
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.
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.
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.
Then click on the location and turn on find
and findOne
.
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:
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:
npx create-next-app@latest
Choose the following options:
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:
npm install react-leaflet react-leaflet-draw axios
Here is what each library will do:
react-leaflet
: This library providesReact
components for building interactive maps using theLeaflet
JavaScript library.react-leaflet-draw
: This library extendsreact-leaflet
by providing components for adding drawing functionalities to the map.axios
: This library will make HTTP requests to fetch data from the Strapi API containing the geographical data.
react-Leaflet
provides bindings between React and Leaflet, leveraging Leaflet to abstract its layers as React components. It does not replace Leaflet but rather works with it. Both leaflet
and 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.
// 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:
// 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
fromreact-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.
// 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 must respond to user interactions, such as clicking on markers or searching for locations. To achieve this, define the relevant event handlers:
// 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.
// 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.
// 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.
// 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:
// 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='© <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:
// 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:
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:
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 and the Strapi backend here.
- https://docs.strapi.io/dev-docs/backend-customization
- https://react-leaflet.js.org/