Build An Address Lookup Feature With React Leaflet
Using the open source LeafletJS package in React to create a map with address search capabilities.
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-leaflet
npm i -D @types/leaflet @types/react-leaflet
At the time of writing these are the package versions used:
Package | Version |
---|---|
react | 18.0.0 |
react-dom | 18.0.0 |
leaflet | 1.9.3 |
react-leaflet | 4.2.0 |
@geoapify/geocoder-autocomplete | 1.5.1 |
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.3/dist/leaflet.css"
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
crossOrigin=""
/>
With that in place let’s create a Map.tsx
component like so:
// Map.tsx
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:
// CustomControl.tsx
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.
// Map.tsx
<MapContainer>
<TileLayer />
<CustomControl />
</MapContainer>
Let’s breakdown what is happening:
- A Leaflet control is an instance of the
L.Control
class that implements theonAdd
andonRemove
methods. - The
useMap
hook returns the root<MapContainer>
reference for any of its child components. - The
L.Control.extend
method returns a class that implements theL.Control
type, and we define the position astopright
. - Within the
onAdd
callback, 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
useEffect
hook we use theaddControl
method to add the instance of our custom control to our<MapContainer>
reference. - The
CustomControl.tsx
component 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-autocomplete
Import the stylesheet into your globals.css
file:
@import "@geoapify/geocoder-autocomplete/styles/minimal.css";
Create an AddressSearch.tsx
component as follows:
// AddressSearch.tsx
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 minimal";
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:
// Map.tsx
<MapContainer>
<TileLayer />
<AddressSearch />
</MapContainer>
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 minimal
CSS class is from the stylesheet provided by the Geocoder Autocomplete package. The HTMLElement
container must have a CSS display property of relative
or absolute
.
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:
// AddressSearch.tsx
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;
};