Using Leaflet in React apps: React Hooks
Last year I wrote an article Using Leaflet in React apps, in which I’ve used class component lifecycle hooks to integrate React components and Leaflet.
Few weeks ago React team proposed new way of creating stateful components — React Hooks:
Hooks are a new feature proposal that lets you use state and other React features without writing a class. They’re currently in React v16.7.0-alpha and being discussed in an open RFC. — React Hooks Docs
UPDATE: Since React 16.8, hooks are part of public API 🎉
Let’s see how to integrate Leaflet and React using Hooks.
Map with marker
Instead of creating Leaflet instances in componentDidMount
lifecycle hook,
it’s now possible to do that in useEffect
hook:
import React from "react";
import L from "leaflet";
function Map() {
React.useEffect(() => {
// create map
L.map("map", {
center: [49.8419, 24.0315],
zoom: 16,
layers: [
L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
attribution:
'© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
}),
],
});
}, []);
return <div id="map"></div>;
}
export default Map;
Note, that I’ve passed an empty array as second argument to useEffect
— that’s
because I want to run that hook only once — after first render. See Optimizing
Performance by Skipping
Effects
for more info.
Before adding marker to map, let’s keep reference to map instance, created earlier. I don’t use class anymore, so it’s not possible to assign properties to class instance. Fortunately, React refs can be used for this purpose:
The
useRef()
Hook isn’t just for DOM refs. The “ref” object is a generic container whosecurrent
property is mutable and can hold any value, similar to an instance property on a class. — React Hooks FAQ
Now let’s add a marker to map. I’ll do it in a separate Effect Hook for two reasons:
- to separate map and marker creation logic
- to define separate list of props that the effect depends on
Basically, I’d like to update marker’s position every time markerPosition
prop
changes, so I’ll pass [markerPosition]
as second argument to marker’s
useEffect
hook.
import React from "react";
import L from "leaflet";
function Map({ markerPosition }) {
// create map
const mapRef = React.useRef(null);
React.useEffect(() => {
mapRef.current = L.map("map", {
center: [49.8419, 24.0315],
zoom: 16,
layers: [
L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
attribution:
'© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
}),
],
});
}, []);
// add marker
const markerRef = React.useRef(null);
React.useEffect(() => {
if (markerRef.current) {
markerRef.current.setLatLng(markerPosition);
} else {
markerRef.current = L.marker(markerPosition).addTo(mapRef.current);
}
}, [markerPosition]);
return <div id="map"></div>;
}
export default Map;
Note, how useEffect
hook allows to replace bothcomponentDidMount
and
componentDidUpdate
lifecycle hooks.
That’s one of the biggest advantages of effects hooks — they are less error-prone. Because side effects are run both on mount and update by default, there is no need to remember about handling component update logic.
Working example:
Map with markers layer
Let’s use Hooks to rewrite second example from my previous article.
Instead of adding marker to map, I’ll add a LayerGroup
to map, and then add
markers to that LayerGroup
instance.
I’ll add LayerGroup
to map in the same way I’ve added marker to map in
previous example — using useEffect
and useRef
hooks.
The only difference would be that LayerGroup
should be added to map only once
— so I’ll pass an empty array as second parameter of useEffect
to make sure
that I’m not creating multiple instances of LayerGroup
.
import React from "react";
import L from "leaflet";
function Map({ markersData }) {
// create map
const mapRef = React.useRef(null);
React.useEffect(() => {
mapRef.current = L.map("map", {
center: [49.8419, 24.0315],
zoom: 16,
layers: [
L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
attribution:
'© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
}),
],
});
}, []);
// add layer
const layerRef = React.useRef(null);
React.useEffect(() => {
layerRef.current = L.layerGroup().addTo(mapRef.current);
}, []);
return <div id="map" />;
}
export default Map;
Now I need to add markers to LayerGroup
and update them whenever markersData
prop changes.
Let’s use a separate useEffect
hook for that:
import React from "react";
import L from "leaflet";
function Map({ markersData }) {
// create map
const mapRef = React.useRef(null);
React.useEffect(() => {
mapRef.current = L.map("map", {
center: [49.8419, 24.0315],
zoom: 16,
layers: [
L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
attribution:
'© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
}),
],
});
}, []);
// add layer
const layerRef = React.useRef(null);
React.useEffect(() => {
layerRef.current = L.layerGroup().addTo(mapRef.current);
}, []);
// update markers
React.useEffect(() => {
layerRef.current.clearLayers();
markersData.forEach((marker) => {
L.marker(marker.latLng, { title: marker.title }).addTo(layerRef.current);
});
}, [markersData]);
return <div id="map" />;
}
Note, that I’ve passed [markersData]
to useEffect
in order to keep layer’s
markers up to date with markers data.
Working example:
Summary
Hooks are less error-prone and easier to understand. They allow to split logic by context instead of splitting logic by lifecycle hooks. That allows to reuse logic between components way easier than using HOCs.
Remember that Hooks are not part of React public API yet, and they may change in the future. But I recommend to try them out and experiment with them in order to be prepared when Hooks are released for public usage.
UPDATE: Since React 16.8, hooks are part of public API 🎉
Written by Andrew Cherniavskii, Software Engineer.