Most developers are familiar with Google Maps and MapBox, but both require accounts to use them, and maybe even entering credit card details. What if you wanted an open-source and free alternative? Here’s where Leaflet steps up to the plate! It’s a light-weight, open-source mapping library that utilizes OpenStreetMap.
In this article, we’ll see how to use React-Leaflet to render Leaflet maps inside of your React app. We’ll show markers with custom icons, and display a popup on the map when clicked. Later, we will see what needs to change to load remote vs local data using SWR.
After adding react-leaflet
to our package.json
file, we’ll need to do a couple small things to get our map displaying correctly. Leaflet requires some CSS to render, and you can do that either by including the CSS link
tag in your head
, or you can copy/paste the CSS from the file below directly into your project:
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""
/>
We need to also make sure to set the width/height of .leaflet-container
that the map renders itself into, otherwise it won’t be visible because the div will have a height of 0px:
.leaflet-container {
width: 100%;
height: 100vh;
}
Once this is done we’re ready to get started! The code below shows the minimal amount required to get a Leaflet map rendering in our React app. We have imported Map
from react-leaflet
(along with some other packages we’ll utilize later on), and we’ll return it from our App
component.
The Map
component requires that we set a center
position, which is an array containing latitude and longitude, along with the default zoom level of the map.
You’ll also notice the TileLayer
component nested inside of Map
. We are required to give attribution to OpenStreetMap, otherwise, all you’ll see is a grey square on the screen:
import React from "react";
import { Map, Marker, Popup, TileLayer } from "react-leaflet";
import { Icon } from "leaflet";
import * as parkData from "./data/skateboard-parks.json";
import "./app.css";
export default function App() {
return (
<Map center={[45.4, -75.7]} zoom={12}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
/>
</Map>
);
}
To display markers on the map we need some data. Our data comes from the city of Ottawa, containing the skateboard parks in the area. We will load this data locally from a JSON file, but to get an idea of what it looks like, here’s an example of two skateparks below:
{
"features": [
{
"type": "Feature",
"properties": {
"PARK_ID": 960,
"NAME": "Bearbrook Skateboard Park",
"DESCRIPTIO": "Flat asphalt surface, 5 components"
},
"geometry": {
"type": "Point",
"coordinates": [-75.3372987731628, 45.383321536272049]
}
},
{
"type": "Feature",
"properties": {
"PARK_ID": 1219,
"NAME": "Bob MacQuarrie Skateboard Park (SK8 Extreme Park)",
"DESCRIPTIO": "Flat asphalt surface, 10 components, City run learn to skateboard programs, City run skateboard camps in summer"
},
"geometry": {
"type": "Point",
"coordinates": [-75.546518086577947, 45.467134581917357]
}
}
]
}
With our data in place, we can map through it inside of the Map
component, returning a Marker
component for each of the park locations. A Marker
requires a position
prop, telling it where to render on the map. This is an array of [latitude, longitude]
, much like the center
prop of the Map
.
In addition to this, I have set up some state. Inside of the onClick
prop, we will set the activePark
when a user clicks on the marker. This will be used later to show some information to the user about a specific skatepark in a map popup:
export default function App() {
const [activePark, setActivePark] = React.useState(null);
return (
<Map center={[45.4, -75.7]} zoom={12}>
{parkData.features.map(park => (
<Marker
key={park.properties.PARK_ID}
position={[
park.geometry.coordinates[1],
park.geometry.coordinates[0]
]}
onClick={() => {
setActivePark(park);
}}
/>
))}
</Map>
);
}
Because we are tracking which skatepark the user has clicked on, if there is an activePark
in our state, we can show a Popup
. The Popup
component shows a little white bubble that can be closed, and much like a Marker
, we’re required to give it a position so it knows where to render on the map. Inside of the Popup
we’re able to pass HTML. This can also be styled using CSS, so feel free to change the look and feel to get it looking exactly like you want.
There is an onClose
prop/event on the Popup
, allowing us to track when the user closes the popup bubble so we can update the state accordingly.
<Map center={[45.4, -75.7]} zoom={12}>
{activePark && (
<Popup
position={[
activePark.geometry.coordinates[1],
activePark.geometry.coordinates[0]
]}
onClose={() => {
setActivePark(null);
}}
>
<div>
<h2>{activePark.properties.NAME}</h2>
<p>{activePark.properties.DESCRIPTIO}</p>
</div>
</Popup>
)}
</Map>
It is easy to customize marker icons in Leaflet. It is done by first using Icon
, imported from leaflet
itself. With that, we can create a new Icon instance, setting the URL location of the image along with its size.
import { Icon } from "leaflet";
const skater = new Icon({
iconUrl: "/skateboarding.svg",
iconSize: [25, 25]
});
The Marker
component has an icon
prop which can be set to the skater
variable that we created.
Using SWR for remote data fetching, we can load our data remotely from an API endpoint. If you have never worked with SWR before, I have an article/video on how it works in React. The truth is that once you have the data, how you display it on the map is no different from local data. We are going to display some crime data provided by the UK police.
I have sliced the data to only render the first 100 crimes in the array because when rendering 1000+ markers, the map slows to a crawl:
// existing imports + new import for SWR
import useSwr from "swr";
const fetcher = (...args) => fetch(...args).then(response => response.json());
export default function App() {
const url =
"https://data.police.uk/api/crimes-street/all-crime?lat=52.629729&lng=-1.131592&date=2019-10";
const { data, error } = useSwr(url, { fetcher });
const crimes = data && !error ? data.slice(0, 100) : [];
return (
<Map center={[52.6376, -1.135171]} zoom={12}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
/>
{crimes.map(crime => (
<Marker
key={crime.id}
position={[crime.location.latitude, crime.location.longitude]}
/>
))}
</Map>
);
}
If you do require thousands of markers, you may want to look at either using Leaflet directly (to see if it can give you some additional performance) or seeing if Google Maps or MapBox are better suited to your needs.
Leaflet and its React counterpart, React-Leaflet, are a fantastic open-source and free mapping alternative to Google Maps and MapBox, no API key required! It is an easy package to work with and one worth trying out. Leaflet is an extremely light library, coming in at only 40kb of JS, and it is used by industry giants such as GitHub, Pinterest, and Etsy. Many more examples can be found on the React-Leaflet website.
The source code shown in this article is available here.
#react-js #react #javascript #web-development