Geographical search with React InstantSearch
This is the React InstantSearch v7 documentation. React InstantSearch v7 is the latest version of React InstantSearch and the stable version of React InstantSearch Hooks.
If you were using React InstantSearch v6, you can upgrade to v7.
If you were using React InstantSearch Hooks, you can still use the React InstantSearch v7 documentation, but you should check the upgrade guide for necessary changes.
If you want to keep using React InstantSearch v6, you can find the archived documentation.
On this page
You can use the geographical search capabilities of Algolia with React InstantSearch by creating a custom widget based on the useGeoSearch()
Hook.
This Hook isn’t tied to any map provider, so you can select and implement any solution.
This guide uses Leaflet as the map provider through its React wrapper, React Leaflet. To learn more about loading and using leaflet, visit the Leaflet documentation.
Dataset
To follow along, you can use a dataset with more than 3,000 airports. You can download the dataset on GitHub. You can download the dataset on GitHub. See Importing from the dashboard to learn how to upload the dataset to Algolia.
1
2
3
4
5
6
7
8
9
10
11
12
{
"objectID": "3797",
"name": "John F Kennedy Intl",
"city": "New York",
"country": "United States",
"iata_code": "JFK",
"links_count": 911,
"_geoloc": {
"lat": 40.639751,
"lng": -73.778925
}
}
To be able to display hits on the map, the latitude and longitude is stored for each airport.
To support location-based filtering and sorting, the latitude and longitude should be stored in the _geoloc
attribute.
Configure index settings
When displaying on a map, you still want the relevance of your search results to be good. To achieve that, you need to configure:
- Searchable attributes—support searching by:
name
,city
,country
andiata_code
. - Custom ranking—use the number of other connected airports
links_count
as a ranking metric. The more connections the better.
1
2
3
4
$index->setSettings([
'searchableAttributes' => ['name', 'city', 'country', 'iata_code'],
'customRanking' => ['desc(links_count)']
]);
Display hits on the map
Add the Leaflet map container to your React InstantSearch app. By default, the map container has height 0, so make sure to set an explicit height, for example, using CSS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// App.tsx
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, SearchBox } from 'react-instantsearch';
import { MapContainer, TileLayer } from 'react-leaflet';
const searchClient = algoliasearch(
'latency',
'6be0576ff61c053d5f9a3225e2a90f76',
);
export function App() {
return (
<InstantSearch searchClient={searchClient} indexName="airports">
<SearchBox placeholder="Search for airports..." />
<MapContainer
style={{ height: '500px' }}
center={[48.85, 2.35]}
zoom={10}
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
</InstantSearch>
);
}
Next, to populate the map with markers, create a custom widget for React InstantSearch, using the useGeoSearch()
Hook you created before.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Airports.tsx
import React from 'react';
import { Marker, Popup } from 'react-leaflet';
import { useGeoSearch } from 'react-instantsearch';
type Airport = {
name: string;
city: string;
country: string;
iata_code: string;
links_count: number;
}
export function Airports() {
const {
items,
} = useGeoSearch<Airport>();
return (
<>
{items.map((item) => (
<Marker
key={item.objectID}
position={item._geoloc}
>
<Popup>
<strong>{item.name}</strong>
<br />
{item.city}, {item.country}
</Popup>
</Marker>
))}
</>
);
}
Finally, import this widget and add it inside <MapContainer>
. You should now see markers representing the airports locations.
Moving the map
To add some interactivity, you can detect users interactions and update the list of airports to match the new viewable area of the map.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ...
import { Marker, Popup, useMapEvents } from 'react-leaflet';
export function Airports() {
const {
items,
refine: refineItems,
} = useGeoSearch();
const onViewChange = ({ target }) => {
refineItems({
northEast: target.getBounds().getNorthEast(),
southWest: target.getBounds().getSouthWest(),
});
};
const map = useMapEvents({
zoomend: onViewChange,
dragend: onViewChange,
});
return (
/* ... */
);
}
Reacting to search query updates
In the current form, if you type a query in the search box, for example, “Italy”, the markers disappear. That’s because the list of markers now show all the airports in Italy, but the map is still in its initial location.
To move the map when the search query changes, use the useSearchBox()
Hook to retrieve the current query, and move the map programmaticaly when it changes.
To not interfere with the manual movement of the map, you need to clear the boundaries refinement when a query changes.
In the onViewChange
event handler, you also need to reset the query and set a flag that instructs the application to not move the map programmatically in this case.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ...
import { useState } from 'react';
import { useSearchBox } from 'react-instantsearch';
export function Airports() {
const { query, refine: refineQuery } = useSearchBox();
const {
items,
refine: refineItems,
currentRefinement,
clearMapRefinement,
} = useGeoSearch();
const [previousQuery, setPreviousQuery] = useState(query);
const [skipViewEffect, setSkipViewEffect] = useState(false);
// When users move the map, clear the query if necessary to only
// refine on the new boundaries of the map.
const onViewChange = ({ target }) => {
setSkipViewEffect(true);
if (query.length > 0) {
refineQuery('');
}
refineItems({
northEast: target.getBounds().getNorthEast(),
southWest: target.getBounds().getSouthWest(),
});
};
const map = useMapEvents({
zoomend: onViewChange,
dragend: onViewChange,
});
// When the query changes, remove the boundary refinement if necessary and
// center the map on the first result.
if (query !== previousQuery) {
if (currentRefinement) {
clearMapRefinement();
}
// `skipViewEffect` allows us to bail out of centering on the first result
// if the query has been cleared programmatically.
if (items.length > 0 && !skipViewEffect) {
map.setView(items[0]._geoloc);
}
setSkipViewEffect(false);
setPreviousQuery(query);
}
return (
/* ... */
);
}