Editor’s note: This post was last updated on 22 October 2021 to improve code and update any outdated information.
Most developers are familiar with Google Maps and MapBox, but both require accounts to use them and can require a paid subscription for certain features. What if you wanted an open source and free alternative? Here’s where Leaflet steps up to the plate!
Leaflet is a lightweight, open source mapping library that utilizes OpenStreetMap, a free editable geographic database.
In this article, we’ll see how to use React Leaflet to render Leaflet maps inside a 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.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Let’s begin by creating a React app, then move into its directory. After that install stable versions of react-leaflet and leaflet with the following commands:
npx create-react-app react-leaflet-demo cd react-leaflet-demo # install react-leaflet and leaflet npm install [email protected] [email protected]
Before rendering the app, replace the "browserslist" values in the package.json file with:
"browserslist": [
">0.2%",
"not dead",
"not op_mini all"
]
Doing so will prevent this error below from popping up:
Module parse failed: Unexpected token (10:41)
File was processed with these loaders:
./node_modules/babel-loader/lib/index.js
You may need an additional loader to handle the result of these loaders.
> | useEffect(function updatePathOptions() {
> | if (props.pathOptions !== optionsRef.current) {
> const options = props.pathOptions ?? {};
> | element.instance.setStyle(options);
> | optionsRef.current = options;
Running npm start should render the app with an affirmative message on your browser.
After adding react-leaflet to our package.json file, we must display our map correctly. React Leaflet requires some CSS to render, and we can either include the CSS link tag in head or we can copy and paste the CSS from the file below directly into the project:
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin="" />
We need to also to set the width and 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 must import Map from react-leaflet (along with some other packages that we’ll utilize later), and we’ll return it from our App component:
import React, {useState} from "react";
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import './App.css';
import { Icon } from "leaflet";
import * as parkData from "./data/skateboard-parks.json";
function App() {
return (
<MapContainer center={[45.4, -75.7]} zoom={12}scrollWheelZoom={false}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
/>
</MapContainer>
);
}
export default App;
The MapContainer 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 MapContainer. We are required to give attribution to OpenStreetMap, otherwise, all you’ll see is a grey square on the screen:
To display markers on the map we need some data. Our data comes from the city of Ottawa, containing the location of skateboard parks in the area. Let’s 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 MapContainer 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 MapContainer.
In addition to this, we must set up some state. Inside the onClick prop, we let’s set the activePark when a user clicks on the marker. We’ll use this later to show some information to the user about a specific skatepark in a map popup:
//App.js
export default function App() {
const [activePark, setActivePark] = useState(null);
return (
<MapContainer center={[45.4, -75.7]} zoom={12} scrollWheelZoom={false}>
{parkData.features.map(park => (
<Marker
key={park.properties.PARK_ID}
position={[
park.geometry.coordinates[1],
park.geometry.coordinates[0]
]}
onClick={() => {
setActivePark(park);
}}
icon={icon}
/>
))}
</MapContainer>
);
}
Because we are tracking which skatepark the user clicks on, if there is an activePark in our state, we can show a popup.
The Popup component shows a little white bubble that can close, 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:
//App.js
<MapContainer center={[45.4, -75.7]} zoom={12} scrollWheelZoom={false}>
{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>
)}
</MapContainer>
It is easy to customize marker icons in Leaflet by 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 that can be set to the skater variable we created.
Using SWR for remote data fetching, we can load our data remotely from an API endpoint. Once we have the data, how we display it on the map is no different from displaying local data. To add this concept, we will 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 rendering more than 1000 markers slows the map:
// existing imports + new import for SWR
import useSwr from "swr";
const fetcher = (...args) => fetch(...args).then(response => response.json());
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 (
<MapContainer center={[52.6376, -1.135171]} zoom={12} scrollWheelZoom={false}>
<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]}
/>
))}
</MapContainer>
);
}
export default App;
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 just under 40kb of JavaScript, and it is used by industry giants such as GitHub, Pinterest, and Etsy.
The source code shown in this article is available here.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Why the future of DX might come from the web platform itself, not more tools or frameworks.

A hands-on test of Claude Code Review across real PRs, breaking down what it flagged, what slipped through, and how the pipeline actually performs in practice.

CSS art once made frontend feel playful and accessible. Here’s why it faded as the web became more practical and prestige-driven.

Learn how inline props break React.memo, trigger unnecessary re-renders, and hurt React performance — plus how to fix them.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now