Build an address lookup feature with React Leaflet
/ 7 min read
Updated:Table of Contents
Add Maps To A Website With Leaflet
Leaflet is an open source JavaScript package for adding interactive maps into your app or webpage. It has a small footprint yet is fully featured, and has a thriving community of plugins to choose from.
The React Leaflet package provides React bindings for the Leaflet package. In this post I demonstrate how to create a map component with address search functionality using the React Leaflet package and Geoapify API.
Through this process, you will learn how to add arbitrary HTML elements as custom controls to a React Leaflet map.
Getting Started With React Leaflet
Let’s begin by rendering a basic map in our React app. Install the required dependencies:
npm i leaflet react-leafletnpm i -D @types/leafletAt the time of writing these are the package versions used:
| Package | Version |
|---|---|
| react | 19.1.0 |
| react-dom | 19.1.0 |
| leaflet | 1.9.4 |
| react-leaflet | 5.0.0 |
| @geoapify/geocoder-autocomplete | 2.1.0 |
The React Leaflet Installation Guide directs us to setup the base Leaflet package first. That involves setting up the stylesheet.
Visit the Leaflet Quick Start Guide to get the latest version of the <link> tag and add it to the <head> of your page:
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossOrigin=""/>With that in place let’s create a Map.tsx component like so:
import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet";
const Map = () => { return ( <MapContainer className="h-80" center={[51.505, -0.09]} zoom={13}> <TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <Marker position={[51.505, -0.09]}> <Popup> A pretty CSS3 popup. <br /> Easily customizable. </Popup> </Marker> </MapContainer> );};
export default Map;This gives us a basic Leaflet map centered on London, England. It comes with a zoom control element and click and drag functionality. We also added a map marker that displays a pop up when clicked.
Make sure to set a height on the component. In this example I styled it with Tailwind CSS: className="h-80".
Creating A Custom Control
Now lets add an arbitrary HTML element as a control overlay to the Leaflet map instance. Create a CustomControl.tsx component like so:
import { Control, DomUtil } from "leaflet";import { useMap } from "react-leaflet";import { useEffect } from "react";
export const LeafletCustomControl = () => { const mapContainer = useMap();
const CustomControl = Control.extend({ options: { position: "topright", }, onAdd: function () { const el = DomUtil.create("div");
el.className = "h-16 w-16 bg-pink-600"; el.innerText = "Click me!";
el.onclick = function () { alert("Hello from a custom Leaflet control!"); };
return el; }, onRemove: function () { return; }, });
const customControl = new CustomControl();
useEffect(() => { mapContainer.addControl(customControl); }, []);
return null;};Add it as a child component of <MapContainer>. You should see a pink square in the top right corner of the map, with a click event handler.
import { MapContainer, TileLayer } from "react-leaflet";import { CustomControl } from "./custom-control"
const Map = () => { return ( <MapContainer className="h-80" center={[51.505, -0.09]} zoom={13}> <TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <Marker position={[51.505, -0.09]}> <Popup> A pretty CSS3 popup. <br /> Easily customizable. </Popup> </Marker> <CustomControl /> </MapContainer> );};
export default Map;Let’s breakdown what is happening:
- A Leaflet control is an instance of the
L.Controlclass that implements theonAddandonRemovemethods. - The
useMaphook returns the root<MapContainer>reference for any of its child components. - The
L.Control.extendmethod returns a class that implements theL.Controltype, and we define the position astopright. - Within the
onAddcallback, we create anHTMLDivElement, add styles and event handlers to the element, and return it from the function. - We instantiate an instance of our custom control.
- Inside a
useEffecthook we use theaddControlmethod to add the instance of our custom control to our<MapContainer>reference. - The
CustomControl.tsxcomponent itself returnsnull.
We now have a React component that can be used with any React Leaflet map by adding it as a child component. More importantly, the control element will be rendered on top of the map tiles, which makes for an intuitive user experience.
Address Search With Geoapify
Let’s make something useful now. We will leverage the Geoapify Address Autocomplete API to add address search functionality to our map. The free tier generously provides 3000 credits for use daily. We will use the Geocoder Autocomplete package which handles making API calls, and rendering of the search suggestions on the page for us.
Before we proceed, you will need to visit the Geoapify platform and register so you can obtain an API key.
Next, install the package for the address search element:
npm i @geoapify/geocoder-autocompleteImport the stylesheet into your globals.css file:
@import "@geoapify/geocoder-autocomplete/styles/minimal.css";Create an AddressSearch.tsx component as follows:
import { GeocoderAutocomplete } from "@geoapify/geocoder-autocomplete";import { Control, DomUtil } from "leaflet";import { useMap } from "react-leaflet";import { useEffect } from "react";
export const AddressSearch = () => { const map = useMap();
const SearchControl = Control.extend({ options: { position: "topright", }, onAdd: function () { const el = DomUtil.create("div");
el.className = "relative";
el.addEventListener("click", (e) => { e.stopPropagation(); });
el.addEventListener("dblclick", (e) => { e.stopPropagation(); });
const autocomplete = new GeocoderAutocomplete(el, "API_KEY", { placeholder: "Enter an address", });
autocomplete.on("select", (location) => { const { lat, lon } = location.properties; map.setView({ lat, lng: lon }, map.getZoom()); });
return el; }, onRemove: function (map: Map) { return; }, });
const searchControl = new SearchControl();
useEffect(() => { map.addControl(searchControl); }, []);
return null;};Let’s add the component as a child of the <MapContainer> component to use the control:
import { MapContainer, TileLayer } from "react-leaflet";import { CustomControl } from "./custom-control"
const Map = () => { return ( <MapContainer className="h-80" center={[51.505, -0.09]} zoom={13}> <TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> <CustomControl /> <AddressSearch /> </MapContainer> );};
export default Map;The GeocoderAutocomplete class accepts as arguments an HTMLElement, the API key, and some optional configurations. Instantiating the class will add the address search component as a child of the HTMLElement supplied as the argument.
Like in the previous example, we add styles to the HTMLElement. The HTMLElement container must have a CSS display property of relative or absolute. The globally imported stylesheet will style the rendered input element with zero configuration.
The Autocomplete component can listen to a number of events. We use the select event to change the map view when a suggestion is selected.
Optimization
Let’s wrap the initialization of the control with the useMemo hook to improve performance.
Return new SearchControl() from useMemo, so that it is available in our useEffect hook:
export const AddressSearch = () => { const map = useMap();
const searchControl = useMemo(() => { const SearchControl = Control.extend({ options: { position: "topright", }, onAdd: function () { const el = DomUtil.create("div");
el.className = "relative minimal round-borders";
el.addEventListener("click", (e) => { e.stopPropagation(); });
el.addEventListener("dblclick", (e) => { e.stopPropagation(); });
const autocomplete = new GeocoderAutocomplete(el, "API_KEY", { placeholder: "Enter an address", });
autocomplete.on("select", (location) => { const { lat, lon } = location.properties; map.setView({ lat, lng: lon }, map.getZoom()); });
return el; }, onRemove: function () { return; }, });
return new SearchControl(); }, []);
useEffect(() => { map.addControl(searchControl); }, [map, searchControl]);
return null;};