Play with Maps in React: Online and Offline using Leaflet libraries

In my previous blog, I started with Maps in React applications using MapBox including custom markers. In this blog, I continue to further delve into using maps but this time I picked a different base library – Leaflet, one of the most powerful open source JS Library for interactive maps.

Leaflet is popular due to its simplicity, performance, usability and is further empowered with a huge variety of available plugins which cover almost every functionality you would require to achieve with maps.

In this blog, we will make use of React-Leaflet to include maps, markers, popups and use the react-leaflet-markercluster library to make clusters of the markers in our React application. We will also see how we can make the map work in offline mode, if the user is not connected to the network, using the leaflet-offline library.

We will use the same example of the latitudes and longitudes used in the last blog and show the list of hospitals with the data of Available beds in each.

As always, let’s start with a new app created using create-react-app and then adding to it, the list of dependencies using npm install.

npm install leaflet  leaflet.markercluster prop-types react-leaflet
react-leaflet-markercluster@next  localforage leaflet-offline --save

This is how my dependencies in package.json look like after the long running npm install completes.

"dependencies": {
   "leaflet": "^1.5.1",
   "leaflet-offline": "^1.1.0",
   "leaflet.markercluster": "^1.4.1",
   "localforage": "^1.7.3",
   "prop-types": "^15.7.2",
   "react": "^16.8.6",
   "react-dom": "^16.8.6",
   "react-leaflet": "^2.4.0",
   "react-leaflet-markercluster": "^2.0.0-rc3",
   "react-scripts": "3.0.1",
   },

Don’t miss to include the @next version for react-leaflet-markercluster.

For the Leaflet JS to work as expected we need to include the below links in public/index.html:

 <link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css" /> 
    <script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"></script>

If you miss to include the above, you will get broken pieces of map but not a proper complete map view.

With the prerequisites setup in place, we can now proceed with defining our Map component and rendering it in the App.js.

Adding a LeafletMap Component with Markers and PopUps

To define a map, first the below imports are to be included:

import { Map , Marker, Popup, TileLayer } from 'react-leaflet';

The local state is initialised with the required coordinates(latitude, longitude),and zoom options for the map:

constructor(props) {
    super(props);
   this.state = {
      lat: 17.44212,
      lng: 78.391384,
      zoom: 15,
      maxZoom: 30
    }
  }

Each aspect of the map comprises of different layers which combine to give the complete view of the map. As we include the code, we will have a layer within the map element within which the map will be shown. The APIs return a set of smaller images ( tiles ) that are displayed together one after the other to render the entire map. For every zoom-in or zoom-out, the same behavior gets repeated to render the view. You may have noticed that in the map view also, the map gets rendered piece by piece.

To establish a layer to display the map, we will use a TileLayer element from the react-leaflet library. 

The TileLayer takes two mandatory attributes which include:

  • A url — The url is used to make the API call and get the tiles data for the map.
  • An attribution — This string is used to attribute to the credits from where the url is being used.

Now the map can be rendered as below with Map layered with the TileLayer and further including layers of the marker data with the PopUp data. The data and approach is similar to what was used in the last blog.

The render function looks like below:

render() {
    const position = [this.state.lat, this.state.lng];
      
    return (
      <div id="map-id">
       <Map center={position} zoom={this,state.zoom}  maxZoom={this.state.maxZoom}
 id="map">
        <TileLayer
          attribution="&copy; <a href=&quot;https://osm.org/copyright&quot;>OpenStreetMap</a> contributors"
          url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
        />
         {markerList.map((marker, index) => {
            let post = [marker.lat, marker.lng];
            return (
              <Marker key={index} position={post}>
                {this.renderPopup(index)}
              </Marker>
            );
          })}
         </Map>
      </div>
    );
  }

An important thing to setup here for the map to get rendered properly is the height of the container for the map, which can be either set using inline style or as a class as below in styles.
.leaflet-container {
  height: 400px;
  width: 100%;
}

The Popups content can be formatted and rendered as below, where marker list is a static array of coordinates and associated data which have to be marked in the map:

renderPopup = (index) =>{
    return (
      <Popup
        tipSize={5}
        anchor="bottom-right"
        longitude={markerList[index].lng}
        latitude={markerList[index].lat}
        >
        <p>
          <strong>{markerList[index].name}</strong>
          <br />
          Available beds:{markerList[index].info}
        </p>
      </Popup>
    );
  }

The rendered local map looks like below(the center coordinates are from a locality called Madhapur in Hyderabad, India):

You can see all the markers and also when you click on the marker, you can see the PopUp. 

If we want to change the standard markers with some custom image or icon, we can define as below:

const customMarker = new L.icon({
  iconUrl: require('../assets/hostpital-building.svg'),
  iconSize: new L.Point(35, 46),
});

The same should be passed into the icon attribute of Marker as below:

<Marker key={index} position={post} icon={customMarker} >
     {this.renderPopup(index)}
 </Marker>

With the custom markers, the map looks like below: 

Now, what if we have closely overlapping markers like below?

This is where we need Clusters, to group the closely occuring marker coordinates and show them as a summary count.

Creating Clusters of Markers

We will be using react-leaflet-markercluster and leaflet-markercluster to achieve the cluster functionality. These libraries have already been listed in the initial dependencies so we are ready to put them to use.

To begin let’s import it into our LeafletMap component. We will also import Leaflet to use it to style the cluster icon:

import  MarkerClusterGroup  from "react-leaflet-markercluster";
import L from "leaflet";

Next we group the markers within the MarkerClusterGroup tag with some additional attributes as below:

<MarkerClusterGroup>
          {markerList.map((marker, index) => {
            let post = [marker.lat, marker.lng];
            return (
              <Marker key={index} position={post}>
                {this.renderPopup(index)}
              </Marker>
            );
          })}
  </MarkerClusterGroup>

Two important points to keep in mind for this library to work here:

  • There should be maxZoom set at the map level:
<Map center={position} zoom={13} maxZoom={20} id="map">
  • Include the library specific css in your application in the preferred format(css, scss or js). I included it in my index.css as below:
@import url("~react-leaflet-markercluster/dist/styles.min.css");

Now if you zoom out of your map, as the markers start overlapping, you can see the collective counts instead of individual markers.

To incorporate better styling and rendering of the cluster icon, you can add a custom icon function as below:

customIconCreateFunction(cluster) {
    return L.divIcon({
      html: `<span>${cluster.getChildCount()}</span>`,
      className: "marker-cluster-custom",
      iconSize: L.point(40, 40, true)
    });
  }

The same needs to be passed as attributes to the <MarkerClusterGroup> as below:

<MarkerClusterGroup
                 iconCreateFunction={this.customIconCreateFunction}
   >
The below class to be added to the styles:
.marker-cluster-custom {
  background: orange;
  border: 3px solid #ededed;
  border-radius: 50%;
  color: darkblue;
  height: 40px;
  line-height: 37px;
  text-align: center;
  width: 40px;
}

You can now see your clusters beautifully colored and also how they appear  as you keep zooming out and get separated into markers as you start zooming in as below:

You can further explore the other options of clusters at Leaflet.markercluster

Now the map is fully functional with custom markers, popup and clusters.

 You can fork and play with the code @sandbox

Making the Map available Offline

We know that our map is made up of layers, so now if I want to be able to view and play with my maps, even when I am not connected to the internet, i.e. offline mode, what should I do?

Just add another offline layer! Let’s see how we can do that.

For this ask, we have included the leaflet-offline and localforage in the initial dependencies.

Include the imports as below:

import localforage from 'localforage';
import 'leaflet-offline';

Set up the offline layer after the application mounts  in componentDidMount as below:

componentDidMount() {
    const map = L.map('map-id');
    const offlineLayer = L.tileLayer.offline('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', localforage, {
    attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
    subdomains: 'abc',
    minZoom: 13,
    maxZoom: 19,
    crossOrigin: true
});
    offlineLayer.addTo(map);
  }

This initialises the map offline content as map-id which is the identifier for the container div of the map and adds it to the map.

<div id="map-id">

Now disconnect from the network and check your map.

Our Map is all set to work on and off the network!

Check out the complete code from git here.

Conclusion

In this blog, we covered different aspects of working with maps in React applications, by including clusters and offline render in addition to Custom Markers and Popups, using the base Leaflet JS libraries and several other React wrapper libraries like React-leaflet and react-leaflet-markercluster which make using the underlying Leaflet library, easy to incorporate in the React ecosystem.

References

View Comments (1)

  • hi thanks for this post. it is very informative. I want to ask one question: is it possible that in React js we can set the map before class or function component? Something like this. I am not using react-leaflet also.

    import React from 'react';
    import L from 'leaflet';
    const style = {
    width: "100%",
    height: "800px"
    };
    let map = L.map('map', {
    center: [49.8419, 24.0315],
    zoom: 16,
    layers: [
    L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
    attribution: '© OpenStreetMap contributors'
    }),
    ]
    }).addTo(map);;

    function App () {
    return ;
    }
    export default App;

This website uses cookies.