API Reference / React InstantSearch / GeoSearch
Signature
<GeoSearch
  google={object}
  children={function}
  // Optional parameters
  initialZoom={number}
  initialPosition={object}
  enableRefine={boolean}
  enableRefineOnMapMove={boolean}
/>
Import
1
import { GeoSearch } from 'react-instantsearch-dom-maps';

About this widget

We released React InstantSearch Hooks, a new InstantSearch library for React. We recommend using React InstantSearch Hooks in new projects or upgrading from React InstantSearch.

The geoSearch widget displays search results on a Google Map. It lets you search for results based on their position and provides some common usage patterns such as “search on map interactions”.

All other geo components must be nested under it. All the options available on the Google Maps class can be provided as props.

Requirements

The API of this widget is different than others in React InstantSearch. It’s component-driven rather than options-driven since it brings more flexibility to the widget. Since the geo search pattern isn’t a use case for every application, it must be installed as a separate package, as follows:

$
npm install --save react-instantsearch-dom-maps

The widget uses the geo search capabilities of Algolia. Your hits must have a _geoloc attribute so they can be displayed on the map.

The feature is currently incompatible with multiple values in the _geoloc attribute (for example, a restaurant with multiple locations). In such cases, you can duplicate your records and use the distinct feature of Algolia to retrieve only the most relevant result.

You are responsible for loading the Google Maps library. We provide the <GoogleMapsLoader /> component to load the library but its usage is not required to use the geo widget. You can use any strategy you want to load Google Maps. You can find more informations in the Google Maps documentation.

Make sure that you explicitly set the height of the map container (see below), otherwise it won’t show.

Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {
  GoogleMapsLoader,
  GeoSearch,
  Control,
  Marker,
} from 'react-instantsearch-dom-maps';

<div style={{ height: 500 }}>
  <GoogleMapsLoader apiKey="GOOGLE_MAPS_API_KEY">
    {google => (
      <GeoSearch google={google}>
        {({ hits }) => (
          <div>
            <Control />
            {hits.map(hit => (
              <Marker key={hit.objectID} hit={hit} />
            ))}
          </div>
        )}
      </GeoSearch>
    )}
  </GoogleMapsLoader>
</div>

Props

google
type: object
Required

A reference to the global google object (usually window.google). See the Google Maps documentation for more information.

1
2
3
<GeoSearch google={window.google}>
  {() => null}
</GeoSearch>
children
type: function
Required

The render function takes an object as argument with the hits inside.

1
2
3
4
5
6
7
8
9
10
11
<GeoSearch
  // ...
>
  {({ hits }) => (
    <ul>
      {hits.map(hit => (
        <li key={hit.objectID}>{hit.objectID}</li>
      ))}
    </ul>
  )}
</GeoSearch>
initialZoom
type: number
default: 1
Optional

By default, the map sets the zoom based on to the markers that are displayed on it. Yet when React InstantSearch refines the results, they may be empty. When it happens, it needs a zoom level to render the map.

1
2
3
4
5
6
<GeoSearch
  // ...
  initialZoom={8}
>
  {() => null}
</GeoSearch>
initialPosition
type: object
default: { lat: 0, lng: 0 }
Optional

By default, the map sets the position based on to the markers that are displayed on it. Yet when React InstantSearch refines the results, they may be empty. When it happens, it needs a position to render the map.

1
2
3
4
5
6
7
8
9
<GeoSearch
  // ...
  initialPosition={{
    lat: 48.88038,
    lng: 2.32695,
  }}
>
  {() => null}
</GeoSearch>
enableRefine
type: boolean
default: true
Optional

If true, the map is used for refining the search. Otherwise, it’s only for display purposes.

1
2
3
4
5
6
<GeoSearch
  // ...
  enableRefine={false}
>
  {() => null}
</GeoSearch>
enableRefineOnMapMove
type: boolean
default: true
Optional

If true, refine is triggered as you move the map.

1
2
3
4
5
6
<GeoSearch
  // ...
  enableRefineOnMapMove={false}
>
  {() => null}
</GeoSearch>

Customize the UI with connectGeoSearch

If you want to create your own UI of the GeoSearch widget or use another UI library, you can use connectors.

Connectors are higher-order components. They encapsulate the logic for a specific kind of widget and they provide a way to interact with the InstantSearch context.

They have an outer component API that we call exposed props, and they provide some other props to the wrapped components which are called the provided props.

It’s a 3-step process:

// 1. Create a React component
class GeoSearch extends Component {
  render() {
    // return the DOM output
  }
}

// 2. Connect the component using the connector
const CustomGeoSearch = connectGeoSearch(GeoSearch);

// 3. Use your connected widget
<CustomGeoSearch />

Create a React component

class GeoSearch extends Component {
  render() {
    const {
      object[] hits,
      object position,
      object currentRefinement,
      boolean isRefinedWithMap,
      function refine,
      function createURL,
    } = this.props;

    // return the DOM output
  }
}

Provided Props

The examples built with the connector use Leaflet to render the map. Make sure to have the library correctly setup before trying the demo. You can find more details in the Leaflet documentation. We picked Leaflet but you can use any library you prefer (e.g., Google Maps, Mapbox, etc.)

hits
type: object[]

The hits that matched the search request.

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
class GeoSearch extends Component {
  markers = [];

  componentDidMount() {
    this.instance = L.map(this.el);

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

  componentDidUpdate() {
    const { hits } = this.props;

    this.markers.forEach(marker => marker.remove());

    this.markers = hits.map(({ _geoloc }) =>
      L.marker([_geoloc.lat, _geoloc.lng]).addTo(this.instance)
    );

    if (this.markers.length) {
      this.instance.fitBounds(L.featureGroup(this.markers).getBounds(), {
        animate: false,
      });
    }
  }

  render() {
    return <div style={{ height: 500 }} ref={c => (this.el = c)} />;
  }
}
position
type: object

The current position of the search, when applicable.

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
class GeoSearch extends Component {
  markers = [];

  componentDidMount() {
    this.instance = L.map(this.el);

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

  componentDidUpdate() {
    const { hits, position } = this.props;

    this.markers.forEach(marker => marker.remove());

    this.markers = hits.map(({ _geoloc }) =>
      L.marker([_geoloc.lat, _geoloc.lng]).addTo(this.instance)
    );

    if (this.markers.length) {
      this.instance.fitBounds(L.featureGroup(this.markers).getBounds(), {
        animate: false,
      });
    } else {
      this.instance.setView(
        position || {
          lat: 48.864716,
          lng: 2.349014,
        },
        12,
        {
          animate: false,
        }
      );
    }
  }

  render() {
    return <div style={{ height: 500 }} ref={c => (this.el = c)} />;
  }
}
currentRefinement
type: object

The current bounding box of the search, with:

  • northEast: { lat: number, lng: number }: the top right corner of the map view.
  • southWest: { lat: number, lng: number }: the bottom left corner of the map view.
isRefinedWithMap
type: boolean

Returns true if the current refinement is set with the map bounds.

refine
type: function

Sets a bounding box to filter the results from the given map bounds. The function accepts an object with:

  • northEast: { lat: number, lng: number }: the top right corner of the map view.
  • southWest: { lat: number, lng: number }: the bottom left corner of the map view.
createURL
type: function

Generates a URL for the corresponding search state.

Create and instantiate your connected widget

<CustomGeoSearch
  // Optional parameters
  defaultRefinement={object}
/>

Exposed Props

defaultRefinement
type: object
Required

Default search state of the widget containing the bounds for the map. The object contains:

  • northEast: { lat: number, lng: number }: the top right corner of the map view.
  • southWest: { lat: number, lng: number }: the bottom left corner of the map view.
1
2
3
4
5
6
7
8
9
10
11
12
<CustomGeoSearch
  defaultRefinement={{
    northEast: {
      lat: 48.871495114865986,
      lng: 2.398494434852978,
    },
    southWest: {
      lat: 48.8432595812564,
      lng: 2.326310825844189,
    },
  }}
/>

Full example

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { connectGeoSearch } from 'react-instantsearch-dom';

class GeoSearch extends Component {
  isUserInteraction = true;
  markers = [];

  componentDidMount() {
    const { refine } = this.props;

    this.instance = L.map(this.el);

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

    this.instance.on('moveend', () => {
      if (this.isUserInteraction) {
        const ne = this.instance.getBounds().getNorthEast();
        const sw = this.instance.getBounds().getSouthWest();

        refine({
          northEast: { lat: ne.lat, lng: ne.lng },
          southWest: { lat: sw.lat, lng: sw.lng },
        });
      }
    });
  }

  componentDidUpdate() {
    const { hits, currentRefinement, position } = this.props;

    this.markers.forEach(marker => marker.remove());

    this.markers = hits.map(({ _geoloc }) =>
      L.marker([_geoloc.lat, _geoloc.lng]).addTo(this.instance)
    );

    this.isUserInteraction = false;
    if (!currentRefinement && this.markers.length) {
      this.instance.fitBounds(L.featureGroup(this.markers).getBounds(), {
        animate: false,
      });
    } else if (!currentRefinement) {
      this.instance.setView(
        position || {
          lat: 48.864716,
          lng: 2.349014,
        },
        12,
        {
          animate: false,
        }
      );
    }
    this.isUserInteraction = true;
  }

  render() {
    const { currentRefinement, refine } = this.props;

    return (
      <div>
        <div style={{ height: 500 }} ref={c => (this.el = c)} />
        {Boolean(currentRefinement) && (
          <button onClick={() => refine()}>Clear map refinement</button>
        )}
      </div>
    );
  }
}

const CustomGeoSearch = connectGeoSearch(GeoSearch);
Did you find this page helpful?