Part 2-Web Mapping with Leaflet & React.js
Quick and easy “how to” for building a Leaflet Web Map Component in a React.js Application
Well howdy! Welcome to Part 2— it has been a while but here we are with one of my favorites — an opensource legend, Leaflet
We are not using React-Leaflet here today which provides the bindings between React & Leaflet… it’s still Leaflet but… different (If you want a tutorial on that let me know).
We are keeping things lightweight by building a simple component to render the map, display some GeoJSON data as a layer and add some basic functionality to demonstrate Leaflet’s capabilities.
Incase you missed my introductory tutorial where we setup the React.js Application we’ll use throughout this series — you are welcome to clone this Github Repo it has all the code you need to get started. If you‘ve been following this series and have the code built out feel free to continue using the application you created.
1. The Pre-Reqs (same in all parts)
I welcome all skill levels willing to embark on their own Web Mapping journey to follow along!
If you haven’t already, you’ll need to install Node.js I recommend installing their latest stable version.
Make sure you have cloned our starter Github Repo or have a functional application ready to go.
Let’s navigate to our react-map-tutorial
application in VSCode (or preferred IDE) using our terminal. We can run our application using npm start
to make sure everything is working as expected — your application should run on localhost:3000
A browser should open by default with your application loaded, but if not simply navigate to http://localhost:3000 and you should see something similar to this (feel free to click around):
2. Getting Started with Leaflet
Great news! You don’t need a token to wield the power of Leaflet as it’s completely open source — so we can just jump right in
- We can install Leaflet
npm i leaflet
Annnnd now we are ready to develop there’s nothing else we need to configure here!
3. Building our Leaflet Component
Let’s create a directory called LeafletComponent
in our component directory react-map-tutorial/src/Components/MapComponents
this will store our scripts needed for this specific map component.
In this new directory let’s create a file called LeafletComponent.js
for our functional map code to render the component — we’ll just use the styling code in our App.css
file to style this component.
LeafletComponent.js
I’ll paste the full code block at the end of this section
Let’s Import everything that we’ll need
// Obviously we need react and some goodies
import React, {useEffect, useState, useRef} from 'react';
// Here's our Mapbox imports
import "leaflet/dist/leaflet.css";
import L from "leaflet";
// Import App styling
import '../../../App.css';
Declare our Leaflet component and set component properties using react useRef hook (We use this to persist values when the component renders and this helps avoid things re-rendering multiple times)
const LeafletComponent= () => {
const mapContainerRef = useRef(null);
const map = useRef(null);
// ... will keep adding code here
};
Inside our Leaflet component after our useRef properties let’s set our component state with our map center coordinates and zoom level — we use the useState react hook here to track these values in the components state (I’ll show you how this can be useful later)
const [lng] = useState(-97.7431);
const [lat] = useState(30.2672);
const [zoom] = useState(2);
Next let’s use the react Hook useEffect to define map initialization properties for when the component mounts. Notice we are assigning properties for our map to tell it how it needs to render with the component initializes.
useEffect(() => {
map.current = L.map(mapContainerRef.current).setView([lat, lng], zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
}).addTo(map.current);
Let’s Pause really fast — these “Hooks” maybe don’t make any sense but useEffect and react hooks are amazing ways to add functionality with as little code as possible. Here is a great explanation of useEffect but don’t worry if you don’t understand straight away just trust the process!
Some misc code to simply cleanup our map when this component is no longer rendered
// Clean up on unmount
return () => map.current.remove();
}, [lat, lng, zoom]);
Finally let’s end our LeafletComponent
function by telling our component it needs to render our map in this map-container
<div>
return (
<div className="map-container" ref={mapContainerRef}/>
);
Lets export our LeafletComponent
at the end of our file so we can import it
export default LeafletComponent;
Our LeafletComponent.js
should look like this
import React, {useEffect, useState, useRef} from 'react';
import "leaflet/dist/leaflet.css";
import L from "leaflet";
// Import styling
import '../../../App.css';
const LeafletComponent = () => {
const mapContainerRef = useRef(null);
const map = useRef(null);
const [lng] = useState(-97.7431);
const [lat] = useState(30.2672);
const [zoom] = useState(2);
useEffect(() => {
map.current = L.map(mapContainerRef.current).setView([lat, lng], zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
}).addTo(map.current);
return () => {
map.current.remove();
}
}, [lat, lng, zoom]);
return (
<div className="map-container" ref={mapContainerRef}/>
);
};
export default LeafletComponent;
If we reload our application and activate the Lealfet tab…nothing happens — nothing has changed — WELL that’s because we need to tell our Tab.js
component it needs to render this component we created:
In Tab.js
replace
<TabContent id="leaflet" activeTab={activeTab}>
<p>Leaflet Works</p>
</TabContent>
with
<TabContent id="leaflet" activeTab={activeTab}>
<LeafletComponent></LeafletComponent>
</TabContent>
Now we reload our application and see that our Leaflet component has an interactive map!
If the map does not fit in your view correctly — feel free to update the App.css
to suit your styling needs:
.map-container {
position: absolute;
height:400px;
width: 100%;
}
(I have a very basic style applied to each map)
4. Adding Data to our Map
In most cases, you won’t hardcode map data; instead, a service usually provides formatted data to your client-side map for display, so for the sake of getting you familiar with react and Leaflet at the same time we’ll be using an Earthquake GeoJSON dataset from Mapbox online.
If you followed Part 1, you might recall that adding a URL to our data in Mapbox GL would fetch and transform it into a map layer automatically. However, with Leaflet, we need to create a function to fetch and store GeoJSON data.
There are many approaches and architectural considerations that need to be made when determining where and how you’ll serve data to your maps BUT for simplicity, we’ll just add an async function to fetch our data for our LeafletComponent
In our LeafletComponent.js
lets go ahead and add to our LeafletComponent
const
// Where we will store our data
const [data, setData] = useState(null);
// Tell our code if it needs to fetch the data or not
const [load, loadData] = useState(true);
In the useEffect
hook add this function
const fetchEarthquakeData = async () => {
const response = await fetch(
"https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
);
if (load) {
const data = await response.json();
// Set our Data
setData(data);
// Tell our code that we don't need to load the data anymore
loadData(false);
}
};
Lets call our fetchEarthquakeData()
function in a map.on('load'), …)
event
And lastly set our data
, load
as dependencies for our useEffect Hook
// ...
return () => {
map.current.remove();
};
}, [lat, lng, zoom, data, load]);
// ...
Our LeafletComponent.js
should look like this now
import React, { useEffect, useState, useRef } from "react";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
// Import styling
import "../../../App.css";
const LeafletComponent = () => {
const mapContainerRef = useRef(null);
const map = useRef(null);
const [lng] = useState(-97.7431);
const [lat] = useState(30.2672);
const [zoom] = useState(2);
const [data, setData] = useState(null);
const [load, loadData] = useState(true);
useEffect(() => {
const fetchEarthquakeData = async () => {
const response = await fetch(
"https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
);
if (load) {
const data = await response.json();
// Set our Data
setData(data);
// Tell our code that we don't need to load the data anymore
loadData(false);
}
};
map.current = L.map(mapContainerRef.current).setView([lat, lng], zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
"© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
}).addTo(map.current);
// Fetch our Earthquake GeoJSON data
map.current.on('load', fetchEarthquakeData());
return () => {
map.current.remove();
};
}, [lat, lng, zoom, data, load]);
return <div className="map-container" ref={mapContainerRef} />;
};
export default LeafletComponent;
As I said there are many ways to achieve this, this is a bit of a patch solution just for the sake of keeping this tutorial simple.
Adding the Earthquake Layer
In our useEffect hook between where we set our L.tileLayer
and set our map.curent.on(‘load’,…)
event let’s declare our earthquakeMarkerOptions
and declare our earthquakeLayer
var earthquakeMarkerOptions = {
radius: 4,
fillColor: "#FF0000",
color: "#FFFFFF",
weight: 1,
opacity: 1,
fillOpacity: 0.8
};
var earthquakeLayer = L.geoJSON(data,{
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, earthquakeMarkerOptions);
}}).addTo(map.current);
And then in our return for our useEffect hook lets remove our layer
return () => {
// ...
earthquakeLayer.remove();
};
If we reload our application we now see our earthquake data — pretty cool right?
LeafletComponent.js
should look like this:
import React, { useEffect, useState, useRef } from "react";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
// Import styling
import "../../../App.css";
const LeafletComponent = () => {
const mapContainerRef = useRef(null);
const map = useRef(null);
const [lng] = useState(-97.7431);
const [lat] = useState(30.2672);
const [zoom] = useState(2);
const [data, setData] = useState({
type: "FeatureCollection",
features: [],
});
const [load, loadData] = useState(true);
useEffect(() => {
const fetchEarthquakeData = async () => {
const response = await fetch(
"https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
);
if (load) {
const data = await response.json();
// Set our Data
setData(data);
// Tell our code that we don't need to load the data anymore
loadData(false);
}
};
map.current = L.map(mapContainerRef.current).setView([lat, lng], zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
"© <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
}).addTo(map.current);
var earthquakeMarkerOptions = {
radius: 4,
fillColor: "#FF0000",
color: "#FFFFFF",
weight: 1,
opacity: 1,
fillOpacity: 0.8
};
var earthquakeLayer = L.geoJSON(data,{
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, earthquakeMarkerOptions);
}}).addTo(map.current);
// Fetch our Earthquake GeoJSON data
map.current.on('load', fetchEarthquakeData());
return () => {
map.current.remove();
earthquakeLayer.remove();
};
}, [lat, lng, zoom, data, load]);
return <div className="map-container" ref={mapContainerRef} />;
};
export default LeafletComponent;
5. Adding Interactivity
We now have a map with data but it lacks personality, I can see where these earthquakes occur but I don’t know anything about them. Well if we look at our earthquake dataset we can see there are some attributes for each earthquake:
{ "id": "ak16994521", "mag": 2.3, "time": 1507425650893, "felt": null, "tsunami": 0 }
So let’s just say when I hover over an earthquake I want to see the ID, the magnitude and the Date Time it occurred.
We’ll create a popup window when a user performs a mouseover
event — all we need to do is update our earthquakeLayer
properties:
var earthquakeLayer = L.geoJSON(data,{
pointToLayer: function (feature, latlng) {
const earthquakeMarker = L.circleMarker(latlng, earthquakeMarkerOptions);
// Use mouseenter to open the popup
earthquakeMarker.on("mouseover", function (e) {
this.bindPopup(`
<strong>${feature.properties.id}</strong><br>
<strong>Magnitude:</strong>
<p>${feature.properties.mag}</p><br>
<strong>Date:</strong>${new Date(feature.properties.time)}`)
.openPopup();
});
// Use mouseleave to close the popup
earthquakeMarker.on("mouseout", function (e) {
this.closePopup();
});
return earthquakeMarker;
}}).addTo(map.current);
You’ll see we are setting our circleMarker
as a const and then on each of those markers when the mouseover
is triggered it will render a popup with a bunch of attributes from the feature in the event — then with mouseout
we kill the popup!
6. To Conclude
Now obviously this is a very basic example — but you can easy it is to get mapping with Leaflet and React.js. Leaflet is an incredible mapping library made even better by the fact it is open source — I encourage you to explore more capabilities https://leafletjs.com/examples.html with it!
As always here is the Github Repo with the completed Sourcecode for this series — Note it’s updated as I complete each section & add functionality to components on demand.
If you would like me to write a tutorial for React-Leaflet please let me know!
Feel free to check me out at https://www.geoscrub.org/
Hang around for Part 3 where we’ll build an OpenLayers Component *Link Coming soon*