Using leaflet.offline with React?

I’m developing a React app, and I’m trying to implement a Leaflet-map with support for offline download of the map tiles. For this I thought of using leaflet.offline (https://github.com/allartk/leaflet.offline), but I’m unsure of how to use this library in React?

How can one use leaflet.offline with React, or are there any React-specific libraries that supports offline download of map tiles using Leaflet?

Thanks for any help!

Answer

Alright, so I never found anyone online who’s got a solution to this, but after some trial-and-error I’ve got this working. The reason why I wanted to be able to download maps for offline use in Leaflet is because I’m making an app for mobile phones with Ionic, Capacitor and React. It’s probably worth mentioning that I’m using typescript as well.

In the interest of others, I’ll post a guide below on how I got this working. Hopefully this will prevent others from going through the same frustrations I did ๐Ÿ˜€

So the problem I had with the Leaflet.offline plugin was that Leaflet.tileLayer.offline or Leaflet.control.saveTiles weren’t recognized (due to the use of React probably). So here are the steps I took to get offline maps working:

1 – I initially was using leaflet.offline via the npm package. We want to alter the plugin so that we are no longer dependent on the plugin’s use of Leaflet.tileLayer.offline or Leaflet.control.saveTiles. What we want is to rather create some methods that returns an instance of these two mentioned classes, and then we can import these methods into our React-components. The simplest way of doing this is to not use the plugin via npm, but downloading the plugin’s files directly. So the first thing you should do is to download a copy of the contents of the leaflet.offline/src folder that are found on the plugin’s GitHub-page (https://github.com/allartk/leaflet.offline/tree/master/src). The files you should download are ControlSaveTiles.js, TileLayerOffline.js and TileManager.js.

2 – Now create a folder in your React project. I named mine LeafletMapOfflinePlugin, but you can name it whatever you want. Then move the files you downloaded into this folder.

3 – Open the TileLayerOffline.js file, and paste the following code at the bottom of the file. This will export a function that can be used to create a new instance of the TileLayerOffline class, so that we no longer depend on L.tileLayer.offline:

// Export a function that generates an offfline tilelayer-object, 
// as the above expansion of L.tileLayer.offline doesn't work
export function generateOfflineTilelayer(url, options) {
  return new TileLayerOffline(url, options)
}

4 – Open the ControlSaveTiles.js file, and paste the following code at the bottom of the file. This will export a function that can be used to create a new instance of the ControlSaveTiles class, so that we no longer depend on L.control.saveTiles:

// Export a function that generates a savetiles-object,
// as the above expansion of L.control.savetiles doesn't work
export function generateControlSavetiles(baseLayer, options) {
  return new ControlSaveTiles(baseLayer, options);
}

5 – Now these can be imported into any React-component by using the following imports:

import { generateOfflineTilelayer } from "<path-to-TileLayerOffline.js>";
import { generateControlSavetiles } from "<path-to-ControlSaveTiles.js>";

6 – A simple example of how to use this in a React component (with TypeScript) can be seen below. Here we store the MapContainer object using useState, and uses this in an useEffect to manually set the tileLayer and control:

import L, { Map } from "leaflet";
import { MapContainer } from "react-leaflet";
import { generateOfflineTilelayer } from "<path-to-TileLayerOffline.js>";
import { generateControlSavetiles } from "<path-to-ControlSaveTiles.js>";
import React, { useState } from "react";


const LeafletMap: React.FC<LeafletMapProps> = () => {
  const [map, setMap] = useState<Map | undefined>();
  
  useEffect(() => {
    if(map){
      const tileLayerOffline = generateOfflineTilelayer(
        "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
        {
          attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
          minZoom: 13,
        }
      );

      tileLayerOffline.addTo(map);

      const controlSaveTiles = generateControlSavetiles(
        tileLayerOffline, 
        {
          zoomlevels: [13, 14, 15, 16], // optional zoomlevels to save, default current zoomlevel
        }
      );

      controlSaveTiles.addTo(map!);
    }
  }, [map]);
  
  return(
    <MapContainer
      style={{ width: "100vw", height: "20vh" }}
      center={[63.446827, 10.421906]}
      zoom={13}
      scrollWheelZoom={false}
      whenCreated={setMap}
    >
    </MapContainer>
  )
}

7 – You should now have a simple Leaflet-map with a control-element that when the “+” (not to be confused with the “+” for zooming. The “+” is the default for the leaflet.offline’s control element, and can be changed by looking at leaflet.offline’s documentation) is clicked, it downloads and saves the map tiles for the area that is currently visible on the map. If you want, you can inspect the download of these tiles by going to the network-tab of your web browser. When the “-” (again, not to be confused by the button for zooming) is clicked, all the downloaded tiles are deleted. See the image below of how the map should look like when using leaflet.offline’s ControlSaveTiles; the control element for saving tiles is located below the the control element for zooming:

Leaflet offline map example

Alright, that about wraps it up. Hopefully this helps someone ๐Ÿ˜€ Also a tip related to Leaflet maps; if you’re having trouble with only a small piece of the map showing, try using map.invalidateSize() (if your mapcontainer is stored as “map”). This will reset and update the device width known to the leaflet-map, which hopefully makes the map render correctly ๐Ÿ˜€