Skip to main content
This is the React InstantSearch v7 documentation. If you’re upgrading from v6, see the upgrade guide. If you were using React InstantSearch Hooks, this v7 documentation applies—just check for necessary changes. To continue using v6, you can find the archived documentation.
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

This guide use a dataset of over 3,000 of the biggest airports in the world.
JSON
{
  "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
  }
}
Latitude and longitude are stored in the record to allow hits to be displayed on the map. You should store them in the _geoloc attribute to enable geo-filtering and geo-sorting. You can download the dataset on GitHub. Have a look at how to import it in Algolia.

Configure index settings

When displaying on a map, you still want the relevance to be good. For that, configure the as follows:
  • Searchable attributes should be set to enable search in the four textual attributes: name, city, country, and iata_code.
  • Custom ranking: use the number of other connected airports links_count as a ranking metric. The more connections the better.
var response = await client.SetSettingsAsync(
  "ALGOLIA_INDEX_NAME",
  new IndexSettings
  {
    SearchableAttributes = new List<string> { "name", "country", "city", "iata_code" },
    CustomRanking = new List<string> { "desc(links_count)" },
  }
);

Display hits on the map

1

Add the Leaflet map container

By default, the map container has height 0, so make sure to set an explicit height, for example, using CSS.
React
// App.tsx
import React from "react";
import { liteClient as 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='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
      </MapContainer>
    </InstantSearch>
  );
}
2

Create custom widget

To populate the map with markers, create a custom widget for React InstantSearch, using the useGeoSearch() Hook you created before.
React
// 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>
      ))}
    </>
  );
}
3

Import custom widget

Import the widget and add it inside MapContainer. You should now see markers representing the airports locations.

Move 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.
React
// ...
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 (
    /* ... */
  )
}

React 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.
React
// ...
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 (
    /* ... */
  );
}
Last modified on January 28, 2026