UI Libraries / Autocomplete / Integrate Autocomplete with React InstantSearch Hooks

Integrate Autocomplete with React InstantSearch Hooks

When you think of search experiences on sites like Amazon (ecommerce) or YouTube (media), you may notice that both sites use an autocomplete experience. It’s the autocomplete, and not just a search input, that powers the search page.

If you have an existing React InstantSearch Hooks implementation, you can create a similar experience by adding Autocomplete to your React InstantSearch Hooks application. Adding Autocomplete to an existing React InstantSearch Hooks implementation lets you enhance the search experience and create a richer, more contextual search. You can use context from the current user and how they interacted with your site, save their recent searches, provide suggested queries, and more. This autocomplete can work as a rich search box in a search page, and a portable all-in-one search experience anywhere else on your site.

This guide shows you how to integrate Autocomplete with React InstantSearch Hooks on your site.

Preview

This guide starts with a new React InstantSearch Hooks application, but you can adapt it to integrate Autocomplete into your existing implementation.

Creating a search page with React InstantSearch Hooks

First, begin with some boilerplate code for the InstantSearch implementation. The easiest way to generate that is to use the create-instantsearch-app command-line utility.

$
$
$
$
$
$
$
npx create-instantsearch-app react-instantsearch-hooks \
--template "React InstantSearch Hooks" \
--app-id "latency" \
--api-key "6be0576ff61c053d5f9a3225e2a90f76" \
--index-name instant_search \
--attributes-to-display name,description \
--attributes-for-faceting categories

The template uses a two-column layout with categories on the left and a search box, hits, and a pagination widget on the right. Simplify it by replacing the content of App.tsx and App.css with the following:

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
74
75
76
77
78
import React from "react";

import algoliasearch from "algoliasearch/lite";
import { Hit as AlgoliaHit } from "instantsearch.js/es/types";

import {
  Highlight,
  Hits,
  InstantSearch,
  Pagination,
  RefinementList,
  SearchBox,
} from "react-instantsearch-hooks-web";

import "./App.css";

const searchClient = algoliasearch(
  "latency",
  "6be0576ff61c053d5f9a3225e2a90f76"
);

type HitProps = {
  hit: AlgoliaHit<{
    name: string;
    image: string;
    brand: string;
    categories: string[];
  }>;
};

function Hit({ hit }: HitProps) {
  return (
    <article className="hit">
      <div className="hit-image">
        <img src={hit.image} alt={hit.name} />
      </div>
      <div>
        <h1>
          <Highlight hit={hit} attribute="name" />
        </h1>
        <div>
          By <strong>{hit.brand}</strong> in{" "}
          <strong>{hit.categories[0]}</strong>
        </div>
      </div>
    </article>
  );
}

export function App() {
  return (
    <div>
      <InstantSearch
        searchClient={searchClient}
        indexName="instant_search"
        routing
      >
        <header className="header">
          <div className="header-wrapper wrapper">
            <nav className="header-nav">
              <a href="/">Home</a>
            </nav>
            <SearchBox />
          </div>
        </header>
        <div className="container wrapper">
          <div>
            <RefinementList attribute="brand" />
          </div>
          <div>
            <Hits hitComponent={Hit} />
            <Pagination />
          </div>
        </div>
      </InstantSearch>
    </div>
  );
}

You now have a working React InstantSearch Hooks application.

The React InstantSearch Hooks <SearchBox> widget doesn’t offer autocomplete features like those seen on YouTube and Amazon. To implement that, replace <SearchBox> with a search box from Algolia’s Autocomplete UI library.

You can store all the logic of Autocomplete in a React component. By leveraging the hooks offered in React InstantSearch Hooks, you can directly interface with InstantSearch in a simple but powerful way.

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
import React from "react";
import {
  createElement,
  Fragment,
  useEffect,
  useRef,
  useState
} from "react";
import { render } from "react-dom";

import { usePagination, useSearchBox } from "react-instantsearch-hooks";
import { autocomplete, AutocompleteOptions } from "@algolia/autocomplete-js";
import { BaseItem } from "@algolia/autocomplete-core";

import "@algolia/autocomplete-theme-classic";

type AutocompleteProps = Partial<AutocompleteOptions<BaseItem>> & {
  className?: string;
};

type SetInstantSearchUiStateOptions = {
  query: string;
};

export function Autocomplete({
  className,
  ...autocompleteProps
}: AutocompleteProps) {
  const autocompleteContainer = useRef<HTMLDivElement>(null);

  const { query, refine: setQuery } = useSearchBox();
  const { refine: setPage } = usePagination();

  const [instantSearchUiState, setInstantSearchUiState] = useState<
    SetInstantSearchUiStateOptions
  >({ query });

  useEffect(() => {
    setQuery(instantSearchUiState.query);
    setPage(0);
  }, [instantSearchUiState]);

  useEffect(() => {
    if (!autocompleteContainer.current) {
      return;
    }

    const autocompleteInstance = autocomplete({
      ...autocompleteProps,
      container: autocompleteContainer.current,
      initialState: { query },
      onReset() {
        setInstantSearchUiState({ query: "" });
      },
      onSubmit({ state }) {
        setInstantSearchUiState({ query: state.query });
      },
      onStateChange({ prevState, state }) {
        if (prevState.query !== state.query) {
          setInstantSearchUiState({
            query: state.query
          });
        }
      },
      renderer: { createElement, Fragment, render },
    });

    return () => autocompleteInstance.destroy();
  }, []);

  return <div className={className} ref={autocompleteContainer} />;
}

You can now remove the original <SearchBox /> component from your React InstantSearch Hooks implementation and replace it with <Autocomplete />.

1
2
3
4
5
6
7
8
9
10
11
12
/* ... */
function App() {
  /* ... */
  return (
    /* ... */
    <Autocomplete
      placeholder="Search products"
      detachedMediaQuery="none"
      openOnFocus
    />
  );
}

This replaces the InstantSearch search box with Autocomplete, and acts exactly like before. But you can now add many more interesting features.

Adding recent searches

When you search on YouTube or Google and come back to the search box later on, the autocomplete displays your recent searches. This pattern lets users quickly access content by using the same path they took to find it in the first place.

Add recent searches to Autocomplete with the @algolia/autocomplete-plugin-recent-searches package. It exposes a createLocalStorageRecentSearchesPlugin function to let you create a recent searches plugin.

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
/* ... */
import { useMemo } from "react";

import { createLocalStorageRecentSearchesPlugin } from "@algolia/autocomplete-plugin-recent-searches";

export function Autocomplete({
  className,
  ...autocompleteProps
}: AutocompleteProps) {
  /* ... */
  const plugins = useMemo(() => {
    const recentSearches = createLocalStorageRecentSearchesPlugin({
      key: "instantsearch",
      limit: 3,
      transformSource({ source }) {
        return {
          ...source,
          onSelect({ item }) {
            setInstantSearchUiState({ query: item.label });
          }
        };
      }
    });

    return [recentSearches];
  }, []);

  useEffect(() => {
    if (!autocompleteContainer.current) {
      return;
    }

    const autocompleteInstance = autocomplete({
      /* ... */
      plugins,
    });
  }, [plugins]);

  return <div className={className} ref={autocompleteContainer} />;
}

Some of this code is abstracted in the following sections to simplify the examples.

Since the recentSearchesPlugin reads from localStorage, you can’t see any recent searches until you perform at least one query. To submit a search, make sure to press Enter on the query. Once you do, you’ll see it appear as a recent search.

Adding Query Suggestions

The most typical pattern you can see on every autocomplete is suggestions. They’re predictions of queries that match what the user is typing and are guaranteed to return results. For example, when typing “how to” in Google, the search engine suggests matching suggestions for the user to complete their query. It’s beneficial on mobile devices, where typing is more demanding than a physical keyboard.

Autocomplete lets you add Query Suggestions with the @algolia/autocomplete-plugin-query-suggestions package. It exposes a createQuerySuggestionsPlugin function to let you create a Query Suggestions plugin.

This plugin requires a Query Suggestions index.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* ... */
import { INSTANT_SEARCH_INDEX_NAME } from './constants';

function App() {
  /* ... */
  return (
    /* ... */
    <InstantSearch
      searchClient={searchClient}
      indexName={INSTANT_SEARCH_INDEX_NAME}
    >
      /* ... */
      <Autocomplete
        searchClient={searchClient}
        placeholder="Search products"
        detachedMediaQuery="none"
        openOnFocus
      />
    </InstantSearch>
  );
}

Debouncing search results

Having two sets of results update as you type generates many UI flashes. This is distracting for the user because two distinct sections compete for their attention.

You can mitigate this problem by debouncing search results.

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
/* ... */
import { debounce } from "@algolia/autocomplete-shared";

export function Autocomplete({
  searchClient,
  className,
  ...autocompleteProps
}: AutocompleteProps) {
  /* ... */
  const debouncedSetInstantSearchUiState = debounce(
    setInstantSearchUiState,
    500
  );

  /* ... */
  useEffect(() => {
    if (!autocompleteContainer.current) {
      return;
    }

    const autocompleteInstance = autocomplete({
      /* ... */
      onStateChange({ prevState, state }) {
        if (prevState.query !== state.query) {
          debouncedSetInstantSearchUiState({
            query: state.query
          });
        }
      },
    });

    return () => autocompleteInstance.destroy();
  }, [plugins]);
}

Supporting categories in Query Suggestions

A key feature of Algolia’s Autocomplete is the ability to pre-configure your InstantSearch page. The Query Suggestions plugin supports categories that you can use to refine the query and the category in a single interaction. This brings users to the correct category without interacting with the <HierarchicalMenu> widget, but with the autocomplete functionality instead.

First, you need to refine on categories, and support categories in the helpers you created at the beginning.

1
2
3
4
/* ... */
<HierarchicalMenu
  attributes={INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTES}
/>

Then, you can update the plugins to forward the category to these helpers.

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
/* ... */
export function Autocomplete({
  searchClient,
  className,
  ...autocompleteProps
}: AutocompleteProps) {
  /* ... */
  const currentCategory = useMemo(() => {
    const category = categories.find(({ isRefined }) => isRefined);
    return category && category.value;
  }, [categories]);

  const plugins = useMemo(() => {
    const recentSearches = createLocalStorageRecentSearchesPlugin({
      key: "instantsearch",
      limit: 3,
      transformSource({ source }) {
        return {
          ...source,
          onSelect({ item }) {
            setInstantSearchUiState({
              query: item.label,
              category: item.category
            });
          }
        };
      }
    });

    const querySuggestions = createQuerySuggestionsPlugin({
      searchClient,
      indexName: INSTANT_SEARCH_QUERY_SUGGESTIONS,
      getSearchParams() {
        return recentSearches.data!.getAlgoliaSearchParams({
          hitsPerPage: 6
        });
      },
      categoryAttribute: [
        INSTANT_SEARCH_INDEX_NAME,
        "facets",
        "exact_matches",
        INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTES[0]
      ],
      transformSource({ source }) {
        return {
          ...source,
          sourceId: "querySuggestionsPlugin",
          onSelect({ item }) {
            setInstantSearchUiState({
              query: item.query,
              category: item.__autocomplete_qsCategory || ""
            });
          },
          getItems(params) {
            if (!params.state.query) {
              return [];
            }

            return source.getItems(params);
          },
        };
      }
    });

    return [recentSearches, querySuggestions];
  }, []);
}

Finally, you can implement the onReset function on your Autocomplete instance to also reset the InstantSearch category.

1
2
3
4
5
6
const autocompleteInstance = autocomplete({
  /* ... */
  onReset() {
    setInstantSearchUiState({ query: "", category: currentCategory });
  },
});

Adding contextual Query Suggestions

For an even richer Autocomplete experience, you can pick up the currently active InstantSearch category and provide suggestions for both this specific category and others. This pattern lets you reduce the search scope to the current category, like an actual department store, or broaden the suggestions to get out of the current category.

Query Suggestions with current InstantSearch category

First, make sure to set your category attribute as a facet in your Query Suggestions index. In this demo, the attribute to facet is instant_search.facets.exact_matches.hierarchicalCategories.lvl0.value.

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/* ... */
const plugins = useMemo(() => {
  /* ... */
  const querySuggestionsInCategory = createQuerySuggestionsPlugin({
    searchClient,
    indexName: INSTANT_SEARCH_QUERY_SUGGESTIONS,
    getSearchParams() {
      return recentSearches.data!.getAlgoliaSearchParams({
        hitsPerPage: 3,
        facetFilters: [
          `${INSTANT_SEARCH_INDEX_NAME}.facets.exact_matches.${INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTES[0]}.value:${currentCategory}`
        ]
      });
    },
    transformSource({ source }) {
      return {
        ...source,
        sourceId: "querySuggestionsInCategoryPlugin",
        onSelect({ item }) {
          setInstantSearchUiState({
            query: item.query,
            category: item.__autocomplete_qsCategory
          });
        },
        getItems(params) {
          if (!currentCategory) {
            return [];
          }

          return source.getItems(params);
        },
        templates: {
          ...source.templates,
          header({ items }) {
            if (items.length === 0) {
              return <Fragment />;
            }

            return (
              <Fragment>
                <span className="aa-SourceHeaderTitle">
                  In {currentCategory}
                </span>
                <span className="aa-SourceHeaderLine" />
              </Fragment>
            );
          }
        }
      };
    }
  });

  const querySuggestions = createQuerySuggestionsPlugin({
    searchClient,
    indexName: INSTANT_SEARCH_QUERY_SUGGESTIONS,
    getSearchParams() {
      if (!currentCategory) {
        return recentSearches.data!.getAlgoliaSearchParams({
          hitsPerPage: 6
        });
      }

      return recentSearches.data!.getAlgoliaSearchParams({
        hitsPerPage: 3,
        facetFilters: [
          `${INSTANT_SEARCH_INDEX_NAME}.facets.exact_matches.${INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTES[0]}.value:-${currentCategory}`
        ]
      });
    },
    categoryAttribute: [
      INSTANT_SEARCH_INDEX_NAME,
      "facets",
      "exact_matches",
      INSTANT_SEARCH_HIERARCHICAL_ATTRIBUTES[0]
    ],
    transformSource({ source }) {
      return {
        ...source,
        sourceId: "querySuggestionsPlugin",
        onSelect({ item }) {
          setInstantSearchUiState({
            query: item.query,
            category: item.__autocomplete_qsCategory || ""
          });
        },
        getItems(params) {
          if (!params.state.query) {
            return [];
          }

          return source.getItems(params);
        },
        templates: {
          ...source.templates,
          header({ items }) {
            if (!currentCategory || items.length === 0) {
              return <Fragment />;
            }

            return (
              <Fragment>
                <span className="aa-SourceHeaderTitle">
                  In other categories
                </span>
                <span className="aa-SourceHeaderLine" />
              </Fragment>
            );
          }
        }
      };
    }
  });

  return [recentSearches, querySuggestionsInCategory, querySuggestions];
}, [currentCategory]);

Next steps

Autocomplete is now the primary method for users to refine React InstantSearch Hooks results. From now on, you’re leveraging the complete Autocomplete ecosystem to bring a state-of-the-art search experience for desktop and mobile.

You can now add Autocomplete everywhere on your site and redirect users to the search page whenever they submit a search or after they select a suggestion. You can also use context from the current page to personalize the autocomplete experience. For example, you could display a preview of matching results in a panel for each suggestion and let InstantSearch provide these results once on the search page.

Did you find this page helpful?