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:
            '&copy; <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 whose current 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:
            '&copy; <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:
            '&copy; <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:
            '&copy; <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.