avatarJennifer Fu

Summary

The provided content is a comprehensive guide on using Leaflet, an open-source JavaScript library, to create interactive maps within React applications, including setting up the environment, displaying maps, adding markers and popups, working with layers, and rendering GeoJSON data dynamically.

Abstract

The article "Exploring Leaflet for Interactive Map in React Apps" delves into the integration of Leaflet, a leading open-source JavaScript library for mobile-friendly interactive maps, into React applications. It begins by explaining web mapping and Leaflet's simplicity and performance across platforms, followed by a step-by-step setup process using Create React App. The guide demonstrates how to display a map centered in San Francisco, detailing the use of tile layers from OpenStreetMap and the implementation of retina tiles for high-resolution displays. It also covers the creation of markers with custom icons and popups, the handling of map initialization errors, and the use of layers to create various map styles, including the integration of Web Map Service (WMS) for professional GIS-generated maps. Furthermore, the article explains how to render GeoJSON data to display geographic features and associated metadata, concluding with a reflection on Leaflet's capabilities and a mention of an alternative approach using react-leaflet components for future exploration.

Opinions

  • The author emphasizes the importance of simplicity, performance, and usability in web mapping, highlighting Leaflet's design philosophy.
  • Leaflet is praised for its efficient operation across major desktop and mobile platforms and its extensibility through plugins.
  • The guide suggests using StrictMode in development to catch potential issues, but also provides a solution for those who prefer to remove it to avoid certain errors.
  • The article promotes the use of GeoJSON for encoding geographic data structures, showcasing Leaflet's ability to render such data dynamically.
  • The author expresses gratitude towards individuals who have contributed to their understanding and implementation of Leaflet features, indicating a collaborative approach to learning and development.
  • The article encourages readers to explore further by mentioning the use of react-leaflet components as an alternative method for implementing maps in React applications.
  • A recommendation is made for an AI service that offers similar capabilities to ChatGPT Plus (GPT-4) at a more affordable price, suggesting the service's cost-effectiveness for users interested in scaling their software startups.

Exploring Leaflet for Interactive Map in React Apps

Leaflet is an open-source JavaScript library for mobile-friendly interactive maps

Image by author

Web mapping or online mapping is the process of using maps, usually created through geographic information systems (GIS) on the Internet, more specifically in the World Wide Web.

Leaflet is the leading open-source JavaScript library for mobile-friendly interactive maps. It is designed with simplicity, performance, and usability in mind. It works efficiently across all major desktop and mobile platforms, and can be extended with lots of plugins.

As a JavaScript library, Leaflet works for all JavaScript applications. In this article, we are going to take a look at how to use it in React apps.

Set Up Leaflet in Create React App

We use Create React App as a base to explore Leaflet. The following command creates a React project:

% yarn create react-app react-map
% cd react-map

Set up leaflet:

% yarn add leaflet

It becomes part of dependencies in package.json:

"dependencies": {
  "leaflet": "^1.9.3"
}

The working environment is ready.

Show a Map With React App

We are going to create an interactive map in a React app. The map is centered at San Francisco, whose coordinates, (37.774929, -122.419418), can be found at https://www.latlong.net/.

Image by author

'leaflet' is imported as L, and a map centered at San Francisco with the initial zoom set to 8 can be created using the following code snippet:

const map = L.map(mapId, {
  center: [37.774929, -122.419418],
  zoom: 8,
});

Web mapping loads map tiles from a tile server. A tiled web map is a map displayed in a web browser by seamlessly joining dozens of individually requested tiles in image or SVG formats.

Stevage, CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons

OpenStreetMap is a project that creates and distributes free geographic data for the world. The following URL shows a map with specified latitude, longitude, and zoom:

https://www.openstreetmap.org/?mlat={latitude}&mlon={longitude}&zoom={zoom}

Go to the link, https://www.openstreetmap.org/?mlat=37.774929&mlon=-122.419418&zoom=8, and it will be redirected to an interactive map, centered at San Francisco (https://www.openstreetmap.org/?mlat=37.774929&mlon=-122.419418&zoom=8#map=8/37.775/-122.419).

Image by author

Besides standard tiles, retina tiles come with a high dpi resolution for a crystal clear map experience on modern displays. The following image shows difference between standard tiles and retina tiles:

Image by author

L.tilelayer(<String> urlTemplate, <TileLayer options> options?) loads and displays tile layers on the map. urlTemplate can be specified to OpenStreetMap as https://tile.openstreetmap.org/{z}/{x}/{y}{r}.png, where {z} is for zoom level, {x} and {y} are for tile coordinates, and the optional {r} adds @2x to the URL to load retina tiles.

The following L.tilelayer loads and displays tile layers from OpenStreetMap, with attribution to the map contributors.

L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}{r}.png', {
  attribution:
    '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
});

Here is the modified src/App.js:

  • At line 2, 'leaflet' is imported as L.
  • At line 3, Leaflet style is imported.
  • At line 6, mapId is defined, as Leaflet requires a div element with a certain id to put the map (line 21).
  • At lines 8–18, useEffect() instantiates the map. – At lines 9–12, the map is created with mapId (line 9), center coordinates (line 10), and the initial zoom (line 11). – At lines 14–17, L.tileLayer loads and displays tile layers on the map.

Leaflet also requires some CSS changes, which are specified in src/index.css:

body {
  margin: 0;
}

#map {
  width: 100vw;
  height: 100vh;
}

.leaflet-container {
  width: 100%;
  height: 100%;
}

Execute yarn start, and we see the following error:

Map.js:1094 Uncaught Error: Map container is already initialized.
    at NewClass._initContainer (Map.js:1094:1)
    at NewClass.initialize (Map.js:136:1)
    at new NewClass (Class.js:24:1)
    at Object.createMap [as map] (Map.js:1750:1)
    at App.js:9:1
    at commitHookEffectListMount (react-dom.development.js:23150:1)
    at invokePassiveEffectMountInDEV (react-dom.development.js:25154:1)
    at invokeEffectsInDev (react-dom.development.js:27351:1)
    at commitDoubleInvokeEffectsInDEV (react-dom.development.js:27330:1)
    at flushPassiveEffectsImpl (react-dom.development.js:27056:1)

It is because useEffect() is called twice under StrictMode even if an empty array is used as a dependency.

Removing StrictMode in src/index.js fixes the issue.

root.render(
  <̶R̶e̶a̶c̶t̶.̶S̶t̶r̶i̶c̶t̶M̶o̶d̶e̶>̶
    <App />
  <̶/R̶e̶a̶c̶t̶.̶S̶t̶r̶i̶c̶t̶M̶o̶d̶e̶>̶
);

Execute yarn start, and we see an interactive map that is centered at San Francisco.

Image by author

Alternatively, we can keep StrictMode in src/index.js, and clean up the map if it is already initialized.

Here is the modified src/App.js.

  • At line 6, savedMap is created to keep the map reference.
  • At lines 10–13, it cleans up savedMap if it is already initialized. – At line 11, the off method removes a previously added listener function. – At line 12, the remove method removes the layer from the map that it is currently active on.
  • At line 19, savedMap is assigned to the newly created map.

Execute yarn start, and it works without error.

Open the inspect window, and we see map tiles that are loaded.

Image by author

Create Markers and Popups on the Map

To mark a single location on the map, Leaflet provides markers. These markers use a standard symbol, or can be customized.

Marker is defined by L.marker(<LatLng> latlng, <Marker options> options?), where options is defined as the type of MarkerOptions:

export interface MarkerOptions extends InteractiveLayerOptions {
  /** Icon instance to use for rendering the marker */
  icon?: Icon | DivIcon | undefined;

  /** Whether the marker is draggable with mouse/touch or not. */
  draggable?: boolean | undefined;
    
  /** 
   * Whether the marker can be tabbed to with a keyboard and clicked by 
   * pressing enter. 
   */
  keyboard?: boolean | undefined;

  /** 
   * Text for the browser tooltip that appear on marker hover (no tooltip 
   * by default). 
   */
  title?: string | undefined;

  /** 
   * Text for the `alt` attribute of the icon image (useful for 
   * accessibility). 
   */
  alt?: string | undefined;

  /** Option for putting the marker on top of all others (or below). */
  zIndexOffset?: number | undefined;

  /** The opacity of the marker. */
  opacity?: number | undefined;

  /** 
   * If `true`, the marker will get on top of others when you hover the 
   * mouse over it. 
   */
  riseOnHover?: boolean | undefined;
    
  /** The z-index offset used for the `riseOnHover` feature. */
  riseOffset?: number | undefined;

  /** `Map pane` where the markers shadow will be added. */
  shadowPane?: string | undefined;

  /** 
   * Whether to pan the map when dragging this marker near its edge or 
   * not. 
   */
  autoPan?: boolean | undefined;
    
  /** 
   * Distance (in pixels to the left/right and to the top/bottom) of the 
   * map edge to start panning the map. 
   */
  autoPanPadding?: PointExpression | undefined;
    
  /** Number of pixels the map should pan by. */
  autoPanSpeed?: number | undefined;

  /** When true, the map will pan whenever the marker is focused. */
  autoPanOnFocus?: boolean | undefined;
}

Marker has an icon, which is a type of BaseIconOptions:

export interface BaseIconOptions extends LayerOptions {
  /**
   * (required) The URL to the icon image (absolute or relative to your 
   * script path).
   */
  iconUrl?: string | undefined;
  
  /**
   * The URL to a retina sized version of the icon image (absolute or 
   * relative to your script path). Used for Retina screen devices.
   */
  iconRetinaUrl?: string | undefined;
  
  /** Size of the icon image in pixels. */
  iconSize?: PointExpression | undefined;
  
  /**
   * The coordinates of the "tip" of the icon (relative to its top left 
   * corner). 
   * The icon will be aligned so that this point is at the marker's 
   * geographical location. Centered by default if size is specified, 
   * also can be set in CSS with negative margins.
   */
  iconAnchor?: PointExpression | undefined;

  /**
   * The coordinates of the point from which popups will "open", 
   * relative to the icon anchor.
   */
  popupAnchor?: PointExpression | undefined;

  /**
   * The coordinates of the point from which tooltips will "open", 
   * relative to the icon anchor.
   */
  tooltipAnchor?: PointExpression | undefined;

  /**
   * The URL to the icon shadow image. If not specified, no shadow image 
   * will be created.
   */
  shadowUrl?: string | undefined;

  /** The URL to the retina icon shadow image. */
  shadowRetinaUrl?: string | undefined;

  /** Size of the shadow image in pixels. */
  shadowSize?: PointExpression | undefined;

  /**
   * The coordinates of the "tip" of the shadow (relative to its top left 
   * corner) (the same as iconAnchor if not specified).
   */
  shadowAnchor?: PointExpression | undefined;

  /**
   * A custom class name to assign to both icon and shadow images. Empty 
   * by default.
   */
  className?: string | undefined;
}

Let’s add a marker to label San Francisco in src/App.js:

  • At line 4, Leaflet distributed marker icon, leaflet/dist/images/marker-icon.png, is imported.
  • At line 5, Leaflet distributed marker shadow image, leaflet/dist/images/marker-shadow.png, is imported.
  • At line 19, map sets the zoom to 12.
  • At lines 28–31, customIcon is created with Leaflet distributed marker icon and shadow image.
  • At lines 33–37, a marker is created using San Francisco coordinates. It uses customIcon (line 34), and sets the title, 'My City' (line 35). The marker is made draggable (line 36).

Execute yarn start, and the map shows a shadowed and draggable marker at San Francisco coordinates.

Image by author

When the marker is hovered on, the title, My City, shows.

Image by author

We can open popups in certain places of a map. A popup can be bound to the marker click, and in addition, it can be programmably opened:

marker.bindPopup(popupContent).openPopup();

A popup can also be standalone.

L.popup(<Popup options> options?, <Layer> source?)
L.popup(<LatLng> latlng, <Popup options> options?)

options is defined as the type of PopupOptions that extends DivOverlayOptions:

export interface PopupOptions extends DivOverlayOptions {
  /** Max width of the popup in pixels. Default is 300. */
  maxWidth?: number | undefined;
  
  /** Min width of the popup in pixels. Default is 50. */
  minWidth?: number | undefined;

  /** 
   * If set, creates a scrollable container of the given height inside a 
   * popup if its content exceeds it.
   */
  maxHeight?: number | undefined;

  /** 
   * Set it to true if you want to prevent users from panning the popup 
   * off of the screen while it is open. Default is false.
   */
  keepInView?: boolean | undefined;

  /** 
   * Controls the presence of a close button in the popup. Default is 
   * true.
   */
  closeButton?: boolean | undefined;

  /** 
   * Set it to false if you don't want the map to do panning animation to 
   * fit the opened popup. Default is true. 
   */
  autoPan?: boolean | undefined;

  /** 
   * The margin between the popup and the top left corner of the map view 
   * after autopanning was performed.
   */
  autoPanPaddingTopLeft?: PointExpression | undefined;

  /** 
   * The margin between the popup and the bottom right corner of the map 
   * view after autopanning was performed.
   */
  autoPanPaddingBottomRight?: PointExpression | undefined;

  /** 
   * Equivalent of setting both top left and bottom right autopan padding 
   * to the same value. Default is Point(5, 5).
   */
  autoPanPadding?: PointExpression | undefined;

  /** 
   * Set it to false if you want to override the default behavior of the 
   * popup closing when another popup is opened. Default is true.
   */
  autoClose?: boolean | undefined;

  /** 
   * Set it if you want to override the default behavior of the popup 
   * closing when user clicks on the map. Defaults to the map's 
   * closePopupOnClick option.
   */
  closeOnClick?: boolean | undefined;

  /** Set it to false if you want to override the default behavior of the 
   * ESC key for closing of the popup.
   */
  closeOnEscapeKey?: boolean | undefined;
}

DivOverlayOptions is defined as following:

export interface DivOverlayOptions {
  /** The offset of the popup position. Default is Point(0, 7). */
  offset?: PointExpression | undefined;

  /** A custom CSS class name to assign to the popup. */
  className?: string | undefined;

  /** Map pane where the popup will be added. Default is 'popupPane'. */
  pane?: string | undefined;

  /** 
   * If true, the popup/tooltip will listen to the mouse events. Default 
   * is false.
   */
  interactive?: boolean | undefined;

  /**
   * Sets the HTML content of the overlay while initializing. If a 
   * function is passed the source layer will be passed to the function. 
   * The function should return a String or HTMLElement to be used in 
   * the overlay.
   */
  content?: string | HTMLElement | ((layer: Layer) => string) | ((layer: Layer) => HTMLElement);
}

The following code snippet binds a popup to the marker, and then opens it:

L.marker([37.774929, -122.419418], {
  icon: customIcon,
  title: 'My City',
  draggable: true,
  }).addTo(map)
    .bindPopup('San Francisco is the commercial, financial, and cultural center of Northern California.')
    .openPopup();

Execute yarn start, and the map has a popup that is bound to the marker.

Image by author

We have shown a marker and a popup. In addition to San Francisco, we are going to have a custom icon to indicate Stanford University at Palo Alto, California.

Here is the custom icon, src/icons8-university-64.png:

https://icons8.com/icon/49494/university">University icon by https://icons8.com">Icons8

Here is the modified src/App.js:

  • At line 6, the custom marker icon, ./icons8-university-64.png, is imported as collegePng.
  • At line 19, the map coordinates are shifted a bit to show both San Francisco and Palo Alto.
  • At line 20, map sets zoom to 10.
  • At lines 29–38, San Francisco marker is created with a popup, where autoClose is set to false (line 37).
  • At lines 40–50, Stanford University marker is created with a popup, where autoClose is set to false (line 49). The custom icon’s size (line 43) and anchor (line 44) are adjusted.

Execute yarn start, and the map has two markers and two popups.

Image by author

Create Various Maps in Layers

Web Map Service (WMS) is a popular way of publishing maps by professional GIS software. This format is similar to map tiles, but more generic and not so well optimized for use in web maps. Therefore, the rendering might be a little slower.

WMS provides alternatives to OpenStreetMap, with different types of maps.

The following L.tilelayer loads and displays tile layers from WMS offered by Mundialis, with layers set to TOPO-WMS for showing world topography.

L.tileLayer.wms('http://ows.mundialis.de/services/service?', {
  layers: 'TOPO-WMS',
})

Usually it is a good idea to use software such as QGIS to see what layers are available in a WMS server in order to see what layers are available.

Here is the modified src/App.js:

  • At lines 5–18, it creates baseMaps that is composed of 4 layers of maps. – At lines 6–8, the layer, Street View, is created from OpenStreetMap. – At lines 9–11, the layer, Topography, is created from WMS, with a layer of 'TOPO-WMS', showing the world topography. – At lines 12–14, the layer, Places, is created from WMS, with a layer of 'OSM-Overlay-WMS', showing the place names. – At lines 15–17, the combined layer, Topography + Places, is created from WMS, with two layers of 'TOPO-WMS,OSM-Overlay-WMS', showing the world topography with the place names on top of it.
  • At line 36, it creates a layer control from baseMaps. The overlay is set to null, and the control is open (collapsed is set to false).
  • At line 37, the layer, Street View, is added to map as the default layer.

Execute yarn start, and we see the default layer, Street View.

Image by author

Click the radio button of Topography, and we see the layer, Topography ('TOPO-WMS').

Image by author

Click the radio button of Places, and we see the layer, Places ('OSM-Overlay-WMS').

Image by author

Click the radio button of Topography + Places, and we see the combined layer, Topography + Places (TOPO-WMS,OSM-Overlay-WMS, where the order matters).

Image by author

Render GeoJSON Dynamically

GeoJSON is a format for encoding a variety of geographic data structures using JSON. A GeoJSON object supports the following types:

  • Feature: It is a spatially bounded entity, including a geometry object and additional properties. For example, the San Francisco area object.
  • FeatureCollection: It is a list of Features, For example, [the San Francisco area object, the Palo Alto area object].
  • Geometry: It is a region of space, including Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, and GeometryCollection.

We write a GeoJSON file as public/example.geojson:

  • At line 2, it specifies type as FeatureCollection.
  • At lines 4–13, it defines Feature for the San Francisco area object. – At lines 6–9, it defines a Point in geometry with San Francisco coordinates. – At lines 10–12, it defines a popupContent in properties.
  • At lines 14–28, it defines Feature for the Palo Alto area object. – At lines 16–19, it defines a Point in geometry with Palo Alto coordinates. – At lines 20–27, it defines a popupContent and an icon in properties.

GeoJSON is rendered by L.geoJSON(<Object> geojson?, <GeoJSON options> options?), where options is defined as the type of GeoJSONOptions:

export interface GeoJSONOptions<P = any, G extends geojson.GeometryObject = geojson.GeometryObject> extends InteractiveLayerOptions {
  /**
   * A Function defining how GeoJSON points spawn Leaflet layers.
   * It is internally called when data is added, passing the GeoJSON point
   * feature and its LatLng.
   *
   * The default is to spawn a default Marker:
   *
   * ```
   * function(geoJsonPoint, latlng) {
   *   return L.marker(latlng);
   * }
   * ```
   */
  pointToLayer?(geoJsonPoint: geojson.Feature<geojson.Point, P>, latlng: LatLng): Layer; // should import GeoJSON typings

  /**
   * PathOptions or a Function defining the Path options for styling GeoJSON lines and polygons,
   * called internally when data is added.
   *
   * The default value is to not override any defaults:
   *
   * ```
   * function (geoJsonFeature) {
   *   return {}
   * }
   * ```
   */
  style?: PathOptions | StyleFunction<P> | undefined;

  /**
   * A Function that will be called once for each created Feature, after it
   * has been created and styled. Useful for attaching events and popups to features.
   *
   * The default is to do nothing with the newly created layers:
   *
   * ```
   * function (feature, layer) {}
   * ```
   */
  onEachFeature?(feature: geojson.Feature<G, P>, layer: Layer): void;

  /**
   * A Function that will be used to decide whether to show a feature or not.
   *
   * The default is to show all features:
   *
   * ```
   * function (geoJsonFeature) {
   *   return true;
   * }
   * ```
   */
  filter?(geoJsonFeature: geojson.Feature<G, P>): boolean;

  /**
   * A Function that will be used for converting GeoJSON coordinates to LatLngs.
   * The default is the coordsToLatLng static method.
   */
  coordsToLatLng?(coords: [number, number] | [number, number, number]): LatLng; // check if LatLng has an altitude property

  /** Whether default Markers for "Point" type Features inherit from group options. */
  markersInheritOptions?: boolean | undefined;
}

Here is the modified src/App.js:

  • At line 8, the state, jsonContent, is created to store the GeoJSON object.
  • At lines 12–17, useEffect() fetches example.geojson to initialize jsonContent.
  • At lines 19–63, useEffect() depends on jsonContent (line 63). If jsonContent is set (line 33), the content is rendered. – At lines 35–48, pointToLayer defines how points are displayed on layers. If a custom icon is defined, it is rendered (lines 36–40). Otherwise, the default icon is rendered (lines 42–47). – At lines 50–56, onEachFeature is defined to show popups if they are defined by a feature. Each popup’s autoClose is set to false (line 53).
  • At line 59, the map’s zoom is automatically adjusted.
  • At line 61, each popup is opened.

Execute yarn start, and the map has two markers and two popups.

Image by author

Conclusion

Leaflet is an open-source JavaScript library for mobile-friendly interactive maps. It is designed with simplicity, performance, and usability in mind. It works efficiently across all major desktop and mobile platforms, and can be extended with lots of plugins.

We have shown how to set up Leaflet in Create React App, and written code to display interactive maps using Leaflet APIs. 'leaflet' is imported as L. L.tileLayer loads and displays map tiles on the map, either from OpenStreetMap or other WMS. A map can be created by L.map with coordinates and zoom, with optional marker and popups. These objects can be hardcoded in the source, or dynamically loaded from GeoJSON files.

In the next article, we use react-leaflet components, a Leaflet wrapper, to implement maps. Both ways work for the same examples, and it depends on your preference.

Thanks for reading.

Thanks Ethan Brown for introducing Leaflet to me, and Sushmitha Aitha and S Sreeram for implementing Leaflet features in Domino products.

Want to Connect?

If you are interested, check out my directory of web development articles.

More content at PlainEnglish.io.

Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

Interested in scaling your software startup? Check out Circuit.

Maps
Leaflet
React
Web Development
Programming
Recommended from ReadMedium