Concepts / Building Search UI / Autocomplete
May. 10, 2019

Autocomplete

Overview

A common pattern in search is to implement a search box with an autocomplete as a first step of the search experience. React InstantSearch doesn’t come with a built-in widget for the autocomplete. But we have a connector ais-autocomplete that lets you use an external autocomplete component.

In this guide we will cover two use cases:

  • a textarea that use an autocomplete to list the users to mention
  • a SearchBox that displays an autocomplete menu linked to a results page

We won’t cover the usage of the connector in a multi-index context in this guide. There is a dedicated section about that in the Multi index search guide. You can find the source code of both examples on GitHub.

Mention with autocomplete

This first use case implements a common pattern used in chat applications. We are going to display a lot of users to mention inside a textarea. To implement this we will use the library Ant Design that provides a Mention component. Once we have this component, we need to wrap it with our ais-autocomplete connector. The connector exposes two interesting props for this use case: hits and refine. hits contains the list of suggestions and refine is a function that takes a query to retrieve our relevant suggestions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { connectAutoComplete } from 'react-instantsearch-dom';
import Mention from 'antd/lib/mention';
import 'antd/lib/mention/style/index.css';

const Mentions = ({ hits, refine }) => (
  <Mention
    style={{ width: '100%', height: 100 }}
    suggestions={hits.map(hit => hit.name)}
    placeholder="Give someone an @-mention"
    notFoundContent="No suggestions"
    onSearchChange={name => refine(name)}
    multiLines
  />
);

export default connectAutoComplete(Mentions);

Once we have our connected component we can use it like any other widget in our application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-dom';
import Mentions from './Mentions';

const searchClient = algoliasearch(
  'YourApplicationID',
  'YourAdminAPIKey'
);

const App = () => (
  <InstantSearch indexName="actors" searchClient={searchClient}>
    <Mentions />
  </InstantSearch>
);

That’s it! You can find the complete example on GitHub.

Results page with autocomplete

This second use case is focused on integrating a SearchBox with an autocomplete linked to a results page. To implement this we use the library React Autosuggest that provides a component to create an autocomplete menu. Once we have this component, we need to wrap it with our ais-autocomplete connector. The connector exposes three interesting props for this use case: hits, currentRefinement and refine. hits contains the list of suggestions, currentRefinement is the current value of the query, and refine is a function that takes a query to retrieve our relevant suggestions.

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
import React, { Component } from 'react';
import { Highlight, connectAutoComplete } from 'react-instantsearch-dom';
import AutoSuggest from 'react-autosuggest';

class Autocomplete extends Component {
  state = {
    value: this.props.currentRefinement,
  };

  onChange = (event, { newValue }) => {
    this.setState({ value: newValue });
  };

  onSuggestionsFetchRequested = ({ value }) => {
    this.props.refine(value);
  };

  onSuggestionsClearRequested = () => {
    this.props.refine();
  };

  getSuggestionValue(hit) {
    return hit.name;
  }

  renderSuggestion(hit) {
    return <Highlight attribute="name" hit={hit} tagName="mark" />;
  }

  render() {
    const { hits } = this.props;
    const { value } = this.state;

    const inputProps = {
      placeholder: 'Search for a product...',
      onChange: this.onChange,
      value,
    };

    return (
      <AutoSuggest
        suggestions={hits}
        onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
        onSuggestionsClearRequested={this.onSuggestionsClearRequested}
        getSuggestionValue={this.getSuggestionValue}
        renderSuggestion={this.renderSuggestion}
        inputProps={inputProps}
      />
    );
  }
}

export default connectAutoComplete(Autocomplete);

When we have our autocomplete component setup we can integrate it in our application. But for that we need to use two instances of InstantSearch. We use two instances because it allows us to configure the number of hits retrieved by the autocomplete differently than the number of results. The huge benefit of it is also to not have the query tied to both instances at the same time. It allows us to clear the suggestions but sill have the query applied on the second instance. Since we have two instances we need a way to sync the query between the two. We use the concept of Virtual widgets to achieve this.

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
import { InstantSearch, Hits, connectSearchBox } from 'react-instantsearch-dom';
import Autocomplete from './Autocomplete';

const VirtualSearchBox = connectSearchBox(() => null);

const searchClient = algoliasearch(
  'YourApplicationID',
  'YourSearchOnlyAPIKey'
);

class App extends Component {
  state = {
    query: '',
  };

  render() {
    const { query } = this.state;

    return (
      <div>
        <InstantSearch indexName="demo_ecommerce" searchClient={searchClient}>
          <Autocomplete />
        </InstantSearch>

        <InstantSearch indexName="demo_ecommerce" searchClient={searchClient}>
          <VirtualSearchBox defaultRefinement={query} />
          <Hits />
        </InstantSearch>
      </div>
    );
  }
}

Now that we are able to apply a query to the second instance, we need a way to retrieve the selected value from the autocomplete. To do that we provide a callback to the component that will be called each time the value is selected. We also provide a callback to reset the query once the value of the autocomplete is empty.

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
class Autocomplete extends Component {
  // ...

  onChange = (event, { newValue }) => {
    if (!newValue) {
      this.props.onSuggestionCleared();
    }

    this.setState({ value: newValue });
  };

  // ...

  render() {
    const { hits, onSuggestionSelected } = this.props;

    // ...

    return (
      <AutoSuggest
        onSuggestionSelected={onSuggestionSelected}
        // ...
      />
    );
  }
}

The last step is to update the value of the query provided to the second instance with the one from the autocomplete. We use our callbacks previously defined on the autocomplete component.

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
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Hits, connectSearchBox } from 'react-instantsearch-dom';
import Autocomplete from './Autocomplete';

const VirtualSearchBox = connectSearchBox(() => null);

const searchClient = algoliasearch(
  'YourApplicationID',
  'YourSearchOnlyAPIKey'
);

class App extends Component {
  state = {
    query: ''
  };

  onSuggestionSelected = (_, { suggestion }) => {
    this.setState({
      query: suggestion.value
    });
  };

  onSuggestionCleared = () => {
    this.setState({
      query: '',
    });
  };

  render() {
    const { query } = this.state;

    return (
      <div>
        <InstantSearch indexName="demo_ecommerce" searchClient={searchClient}>
          <Autocomplete
            onSuggestionSelected={this.onSuggestionSelected}
            onSuggestionCleared={this.onSuggestionCleared}
          />
        </InstantSearch>

        <InstantSearch indexName="demo_ecommerce" searchClient={searchClient}>
          <VirtualSearchBox defaultRefinement={query} />
          <Hits />
        </InstantSearch>
      </div>
    );
  }
}

That’s it! You can find the complete example on GitHub.

Did you find this page helpful?