Guides / Building Search UI

Upgrading React InstantSearch

Migrate from React InstantSearch v6 to React InstantSearch v7

React InstantSearch v7 is based on InstantSearch.js and lets you fully control the rendering. With React InstantSearch v7, you can create your own UI components using React Hooks.

This guide helps you migrate from React InstantSearch v6 to React InstantSearch v7. If you’re using React InstantSearch < 6, upgrade to v6 first.

Packages

React InstantSearch v7 is available as two packages:

  • react-instantsearch: it provides Hooks, DOM-based components and server utilities. This is the recommended package to use if you are building a web app.
  • react-instantsearch-core: the core package with all non-DOM APIs such as Hooks, server utilities, and the <InstantSearch> component. All these APIs are re-exported in react-instantsearch. You should use this package if you are building a React Native app, or writing your own UI components.

The following packages from React InstantSearch v6 are no longer available as of React InstantSearch v7:

  • react-instantsearch-dom: use react-instantsearch instead.
  • react-instantsearch-native: use react-instantsearch-core instead.

Codemods

React InstantSearch provides a codemod to automatically update your code and use the new package names and APIs from React InstantSearch v7.

What the codemod does:

  • Replace react-instantsearch-dom imports with react-instantsearch.
  • Update most prop names and their values.
  • Add TODO comments for the parts that need manual changes, such as replacing the defaultRefinement props, and some <InstantSearch> props.
  • Update all translations keys to their new names.
  • Replace connectors with their corresponding Hook.

What the codemod doesn’t do:

  • Replace react-instantsearch-native imports. There’s no direct equivalent in React InstantSearch v7. You need to update your code manually using react-instantsearch-core.
  • Update custom components.
  • Update types for TypeScript users.

Codemods are a starting point for your migration, to complement with the step-by-step guide below.

You should run the codemod on a separate Git branch, then manually review the changes before deploying them.

Assuming you have a src/ folder with your React InstantSearch v6 code, you can run the codemod with the following command:

1
npx @codeshift/cli -e js,jsx,ts,tsx --packages 'instantsearch-codemods#ris-v6-to-v7' src/

Once the command completes, you might find annotations on parts of the code you need to update manually. Search for TODO (Codemod generated) in your codebase, which will point you to the right sections of this guide.

Codemods might output code that doesn’t follow your coding style. Make sure to run your code formatter before committing the changes.

1
2
3
yarn prettier --write 'src/**/*.{js,jsx,ts,tsx}'
# or
yarn eslint --fix src/

Components

React InstantSearch v7 provides most of the same UI components as React InstantSearch. When no UI component is available, you can use the matching Hooks to build it yourself.

React InstantSearch v6 React InstantSearch v7
<Breadcrumb> <Breadcrumb>
Prop changes (see step-by-step)
<ClearRefinements> <ClearRefinements>
Prop changes (see step-by-step)
<Configure> <Configure>
No changes
<CurrentRefinements> <CurrentRefinements>
Prop changes (see step-by-step)
<DynamicWidgets> <DynamicWidgets>
No changes
<EXPERIMENTAL_Answers> useConnector(connectAnswers)
No UI component
EXPERIMENTAL_useAnswers() useConnector(connectAnswers)
No UI component
<ExperimentalConfigureRelatedItems> useConnector(connectConfigureRelatedItems)
No UI component
<HierarchicalMenu> <HierarchicalMenu>
Prop changes (see step-by-step)
<Highlight> <Highlight>
Prop changes (see step-by-step)
<Hits> <Hits>
No changes
<HitsPerPage> <HitsPerPage>
Prop changes (see step-by-step)
<Index> <Index> No changes
<InfiniteHits> <InfiniteHits>
Prop changes (see step-by-step)
<InstantSearch> <InstantSearch>
Prop changes (see step-by-step)
<Menu> <Menu>
Prop changes (see step-by-step)
<MenuSelect> useMenu()
No UI component
<NumericMenu> useConnector(connectNumericMenu)
No UI component
<Pagination> <Pagination>
Prop changes (see step-by-step)
<Panel> No equivalent available
<PoweredBy> <PoweredBy>
Prop changes (see step-by-step)
<QueryRuleContext> useQueryRules()
No UI component
<QueryRuleCustomData> useQueryRules()
No UI component
<RangeInput> <RangeInput>
Prop changes (see step-by-step)
<RangeSlider> useRange()
No UI component
<RatingMenu> useMenu()
No UI component
<RefinementList> <RefinementList>
Prop changes (see step-by-step)
<RelevantSort> useConnector(connectRelevantSort)
No UI component
<ScrollTo> No equivalent available
<SearchBox> <SearchBox>
Prop changes (see step-by-step)
<Snippet> <Snippet>
Prop changes (see step-by-step)
<SortBy> <SortBy>
Prop changes (see step-by-step)
<Stats> <Stats>
Prop changes (see step-by-step)
<ToggleRefinement> <ToggleRefinement>
Prop changes (see step-by-step)
<VoiceSearch> useConnector(connectVoiceSearch)
No UI component
Replace rootURL with rootPath

The <Breadcrumb> component now takes an optional rootPath prop which replaces rootURL.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <Breadcrumb
-     rootURL="/audio"
+     rootPath="Audio"
    />
  );
}
Replace translations

The translations keys have changed.

1
2
3
4
5
6
7
8
9
10
function Search() {
  return (
    <Breadcrumb
      translations={{
-       rootLabel: 'Home',
+       rootElementText: 'Home',
      }}
    />
  );
}

<ClearRefinements>

Replace clearsQuery with includedAttributes or excludedAttributes

The <ClearRefinements> component now takes optional includedAttributes and excludedAttributes prop which replace clearsQuery.

By default, the component ignores the query parameter.

If clearsQuery is set to false in your app (or not set at all), you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <ClearRefinements
-     clearsQuery={false}
    />
  );
}

If clearsQuery is set to true in your app, you can remove query from excludedAttributes.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <ClearRefinements
-     clearsQuery
+     excludedAttributes={[]}
    />
  );
}

Alternatively, you can add query to includedAttributes if you’re already hand-picking what attributes to clear. Keep in mind that when you specify includedAttributes, only those attributes get cleared.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <ClearRefinements
-     clearsQuery={false}
+     includedAttributes={['query']}
    />
  );
}

The includedAttributes and excludedAttributes props can’t be used together.

Replace translations

The translations keys have changed.

1
2
3
4
5
6
7
8
9
10
function Search() {
  return (
    <ClearRefinements
      translations={{
-       reset: 'Clear all filters',
+       resetButtonText: 'Clear all',
      }}
    />
  );
}

<CurrentRefinements>

Replace clearsQuery with includedAttributes or excludedAttributes

The <CurrentRefinements> component now takes optional includedAttributes and excludedAttributes prop which replace clearsQuery.

By default, the component ignores the query parameter. If clearsQuery is set to true in your app, you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <CurrentRefinements
-     clearsQuery
    />
  );
}

If clearsQuery is set to false in your app (or not set at all), you can remove query from excludedAttributes.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <CurrentRefinements
-     clearsQuery={false}
+     excludedAttributes={[]}
    />
  );
}

Alternatively, you can add query to includedAttributes if you’re already hand-picking what attributes to clear. Keep in mind that when you specify includedAttributes, only those attributes get cleared.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <CurrentRefinements
-     clearsQuery={false}
+     includedAttributes={['query']}
    />
  );
}

The includedAttributes and excludedAttributes props can’t be used together.

<HierarchicalMenu>

Replace defaultRefinement with initialUiState on <InstantSearch>

The <HierarchicalMenu> component no longer accepts a defaultRefinement prop. Instead, specify an initial UI state on your <InstantSearch> component. Replace YourIndexName with the name of your Algolia index.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       YourIndexName: {
+         hierarchicalMenu: {
+           'hierarchicalCategories.lvl0': [
+             'Audio > Home Audio',
+           ],
+         },
+       },
+     }}
    >
      <HierarchicalMenu
-       defaultRefinement="Audio > Home Audio"
      />
    </InstantSearch>
  );
}
Replace facetOrdering with sortBy

By default, the component sorts categories with the sortBy prop using the rules of renderingContent.facetOrdering when set, and falls back on ascending name ("name:asc").

If facetOrdering is set to true in your app, you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <HierarchicalMenu
-     facetOrdering
    />
  );
}

If facetOrdering is set to false in your app, you can set the sortBy prop with the sorting annotation of your choice, or using a custom sorting function.

1
2
3
4
5
6
7
8
9
10
11
function Search() {
  return (
    <HierarchicalMenu
+     sortBy={['name:asc']}
+     // or
+     sortBy={(a, b) => {
+       return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? -1 : 1;
+     }}
    />
  );
}
Replace translations

The translations keys have changed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Search() {
  return (
    <HierarchicalMenu
      translations={{
-       showMore(expanded) {
-         return expanded ? 'Show less' : 'Show more';
-       },
+       showMoreButtonText({ isShowingMore }) {
+         return isShowingMore ? 'Show less' : 'Show more';
+       },
      }}
    />
  );
}

<Highlight>

Replace tagName with highlightedTagName

The <Highlight> component now takes an optional highlightedTagName prop which replaces tagName.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <Highlight
-     tagName="span"
+     highlightedTagName="span"
    />
  );
}

The default value also changed from "em" to "mark" for highlightedTagName (formerly tagName). If tagName is to "mark" in your app, you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <Highlight
-     tagName="mark"
    />
  );
}

Conversely, if you were relying on the default value for tagName, you should now set it explicitly.

1
2
3
4
5
6
7
function Search() {
  return (
    <Highlight
+     highlightedTagName="em"
    />
  );
}

<HitsPerPage>

Replace defaultRefinement with default in items

The <HitsPerPage> component no longer takes a required defaultRefinement prop. Instead, you can specify which item is selected by default by specifying a boolean default property in the items prop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Search() {
  return (
    <HitsPerPage
-     defaultRefinement={5}
      items={[
        {
          label: '5 hits per page',
          value: 5,
+         default: true,
        },
        {
          label: '10 hits per page',
          value: 10,
        },
      ]}
    />
  );
}

<InfiniteHits>

Import createInfiniteHitsSessionStorageCache from instantsearch.js

The built-in sessionStorage implementation for the cache prop are now available from the instantsearch.js package.

1
2
-import { createInfiniteHitsSessionStorageCache } from 'react-instantsearch-dom';
+import { createInfiniteHitsSessionStorageCache } from 'instantsearch.js/es/lib/infiniteHitsCache';
Replace translations

The translations keys have changed.

1
2
3
4
5
6
7
8
9
10
11
12
function Search() {
  return (
    <InfiniteHits
      translations={{
-       loadPrevious: 'Load previous',
+       showPreviousButtonText: 'Load previous',
-       loadMore: 'Load more',
+       showMoreButtonText: 'Load more',
      }}
    />
  );
}

<InstantSearch>

Replace searchState with initialUiState

The <InstantSearch> component now takes optional initialUiState prop which replaces searchState.

Replace YourIndexName with the name of your Algolia index.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Search() {
  return (
    <InstantSearch
-     searchState={{
-       query: 'iphone',
-       hitsPerPage: 5,
-     }}
+     initialUiState={{
+       YourIndexName: {
+         query: 'iphone',
+         hitsPerPage: 5,
+       },
+     }}
    >
      {/* … */}
    </InstantSearch>
  );
}

To provide an initial state, you must add the corresponding widgets to your implementation. In the previous example, you need to mount a <SearchBox> component and a <HitsPerPage> component (either built-in or virtual) for applying state to queries.

You must nest the state passed to initialUiState under the index name it applies to, even if your implementation targets a single index. If you’re doing multi-index, each piece of state must be nested under its own index. To share state between indices, you must repeat it.

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
function Search() {
  return (
    <InstantSearch
-     searchState={{
-       query: 'phone',
-       indices: {
-         index1: {
-           refinementList: {
-             brand: ['Apple'],
-           },
-         },
-         index2: {
-           refinementList: {
-             brand: ['Samsung'],
-           },
-         },
-       }
-     }}
+     initialUiState={{
+       index1: {
+         query: 'phone',
+         refinementList: {
+           brand: ['Apple'],
+         },
+       },
+       index2: {
+         query: 'phone',
+         refinementList: {
+           brand: ['Samsung'],
+         },
+       },
+     }}
    >
      {/* … */}
    </InstantSearch>
  );
}
Replace onSearchStateChange with onStateChange

If you were using onSearchStateChange to control the instance and react to state changes, you can replace it with onStateChange. Using this prop makes you responsible for updating the state, using the exposed uiState and its setter setUiState.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Search() {
  return (
    <InstantSearch
-     onSearchStateChange={(searchState) => {
-       // Custom logic
-     }}
+     onStateChange={({ uiState, setUiState }) => {
+       // Custom logic
+       setUiState(uiState);
+     }}
    >
      {/* … */}
    </InstantSearch>
  );
}
Replace refresh prop with refresh from useInstantSearch()

The refresh prop was removed. If you need to manually update the cache and refresh the frontend, you can replace the refresh with an imperative call to refresh from the useInstantSearch() Hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Search() {
+ const [shouldRefresh] = useState(false);
+ const { refresh } = useInstantSearch();

+ useEffect(() => {
+   refresh();
+ }, [shouldRefresh]);

  return <>{/* Your JSX */}</>;
}

function App() {
  return (
    <InstantSearch
-     refresh
    >
      <Search />
    </InstantSearch>
  );
}
Replace resultsState, findResultsState and onSearchParameters with the new server-side rendering APIs

The server APIs have been simplified. If you were using resultsState, findResultsState and onSearchParameters for server-side rendering, check the server-side rendering section to migrate.

If you were using onSearchParameters for another use case than server-side rendering, please reach out.

Move createURL in routing

If you were using createURL to manipulate the URL, you can move it in the routing prop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+import { history } from 'instantsearch.js/es/lib/routers';

 function Search() {
   return (
     <InstantSearch
-      createURL={(searchState) => `?q=${searchState.query}`}
       routing={{
         router: history({
           // …
           createURL({ qsModule, routeState, location }) {
             return `?q=${routeState.query}`;
           }
         }),
       }}
     >
       {/* … */}
     </InstantSearch>
   );
 }

For more in-depth guidance and examples with various routing methods, check the routing guide.

Replace defaultRefinement with initialUiState on <InstantSearch>

The <Menu> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         menu: {
+           'categories': [
+             'Apple',
+           ],
+         },
+       },
+     }}
    >
      <Menu
        attribute="categories"
-       defaultRefinement="Apple"
      />
    </InstantSearch>
  );
}
Replace facetOrdering with sortBy

By default, the component sorts categories with the sortBy prop using the rules of renderingContent.facetOrdering when set, and falls back on refined item and ascending name (["isRefined", "name:asc"]).

If facetOrdering is set to true in your app, you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <Menu
-     facetOrdering
    />
  );
}

If facetOrdering is set to false in your app, you can set the sortBy prop with the sorting annotation of your choice, or using a custom sorting function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Search() {
  return (
    <Menu
+     sortBy={['isRefined', 'name:asc']}
+     // or
+     sortBy={(a, b) => {
+       if (a.isRefined && b.isRefined) {
+         return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? -1 : 1;
+       }
+
+       return a.isRefined ? -1 : 1;
+     }}
    />
  );
}
Replace searchable with a custom implementation

The <Menu> widget isn’t searchable. You can create a searchable version by using a custom widget with the useRefinementList() connector.

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
import {
  useRefinementList,
  useInstantSearch,
} from 'react-instantsearch';

function Menu({ attribute, ...props }) {
  const { setIndexUiState } = useInstantSearch();
  const { items, searchForItems, isFromSearch } = useRefinementList({
    attribute,
  });
  const [query, setQuery] = React.useState('');

  function refine(value: string) {
    setQuery('');
    setIndexUiState((uiState) => ({
      ...uiState,
      refinementList: {
        ...uiState.refinementList,
        [attribute]: [value],
      },
    }));
  }

  return (
    <div {...props}>
      <input
        type="search"
        value={query}
        onChange={(event) => {
          const nextValue = event.target.value;
          setQuery(nextValue);
          searchForItems(nextValue);
        }}
      />
      <ul>
        {items.map((item) => (
          <li key={item.label}>
            <label>
              <input
                type="radio"
                checked={item.isRefined}
                onChange={() => refine(item.value)}
              />
              {isFromSearch ? (
                <Highlight hit={mapToHit(item)} attribute="highlighted" />
              ) : (
                item.label
              )}
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
}

function mapToHit(item: RefinementListItem): AlgoliaHit<RefinementListItem> {
  return {
    ...item,
    _highlightResult: {
      highlighted: {
        value: item.highlighted,
        matchLevel: 'full',
        matchedWords: [],
      },
    },
    __position: 0,
    objectID: '',
  };
}
Replace translations

The translations keys have changed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Search() {
  return (
    <HierarchicalMenu
      translations={{
-       showMore(expanded) {
-         return expanded ? 'Show less' : 'Show more';
-       },
+       showMoreButtonText({ isShowingMore }) {
+         return isShowingMore ? 'Show less' : 'Show more';
+       },
      }}
    />
  );
}

<Pagination>

Replace defaultRefinement with initialUiState on <InstantSearch>

The <Pagination> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         page: 2,
+       },
+     }}
    >
      <Pagination
-       defaultRefinement={2}
      />
    </InstantSearch>
  );
}
Replace translations

The translations keys have changed.

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
function Search() {
  return (
    <Pagination
      translations={{
-       previous: 'Previous page',
+       previousPageItemText: 'Previous page',
-       next: 'Next page',
+       nextPageItemText: 'Next page',
-       first: 'First page',
+       firstPageItemText: 'First page',
-       last: 'Last page',
+       lastPageItemText: 'Last page',
-       page: (currentRefinement) => `Page ${currentRefinement}`,
+       pageItemText: ({ currentPage }) => `Page ${currentPage}`,
-       ariaPrevious: 'Previous page',
+       previousPageItemAriaLabel: 'Previous page',
-       ariaNext: 'Next page',
+       nextPageItemAriaLabel: 'Next page',
-       ariaFirst: 'First page',
+       firstPageItemAriaLabel: 'First page',
-       ariaLast: 'Last page',
+       lastPageItemAriaLabel: 'Last page',
-       ariaPage: (currentRefinement) => `Go to page ${currentRefinement}`,
+       pageItemAriaLabel: ({ currentPage }) => `Go to page ${currentPage}`,
      }}
    />
  );
}

<PoweredBy>

Replace translations with a custom implementation

The Algolia logo is now a single image, including the “Search by”, therefore the translations prop was removed. If you need to translate the image, you can use a custom widget with the usePoweredBy() connector.

1
2
3
4
5
6
7
8
9
10
import { usePoweredBy } from 'react-instantsearch';

function CustomPoweredBy() {
  const { url } = usePoweredBy();

  // Download and customize the "Search by Algolia" logo for light and dark themes.
  // https://algolia.frontify.com/d/1AZwVNcFZiu7/style-guide#/basics/algolia-logo

  return <>{/* Your JSX */}</>;
}

<RangeInput>

Replace defaultRefinement with initialUiState on <InstantSearch>

The <RangeInput> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         range: { min: 10, max: 500 },
+       },
+     }}
    >
      <RangeInput
-       defaultRefinement={{ min: 10, max: 500 }}
      />
    </InstantSearch>
  );
}
Replace translations

The translations keys have changed.

1
2
3
4
5
6
7
8
9
10
11
12
function Search() {
  return (
    <RangeInput
      translations={{
-       submit: 'Apply',
+       submitButtonText: 'Apply',
-       separator: '-',
+       separatorElementText: '-',
      }}
    />
  );
}

<RefinementList>

Replace defaultRefinement with initialUiState on <InstantSearch>

The <RefinementList> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         refinementList: {
+           brand: ['Apple'],
+         },
+       },
+     }}
    >
      <RefinementList
        attribute="brand"
-       defaultRefinement={['Apple']}
      />
    </InstantSearch>
  );
}
Replace facetOrdering with sortBy

By default, the component sorts categories with the sortBy prop using the rules of renderingContent.facetOrdering when set, and falls back on refined item, descending count, and ascending name (["isRefined", "count:desc", "name:asc"]).

If facetOrdering is set to true in your app, you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <RefinementList
-     facetOrdering
    />
  );
}

If facetOrdering is set to false in your app, you can set the sortBy prop with the sorting annotation of your choice, or using a custom sorting function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Search() {
  return (
    <RefinementList
+     sortBy={['isRefined', 'count:desc', 'name:asc']}
+     // or
+     sortBy={(a, b) => {
+       if (a.isRefined && b.isRefined) {
+         if (a.count === b.count) {
+           return a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase() ? -1 : 1;
+         }
+
+         return a.count > b.count ? -1 : 1;
+       }
+
+       return a.isRefined ? -1 : 1;
+     }}
    />
  );
}
Replace translations

The translations keys have changed. Additionally, the placeholder translation now has its own top-level prop, searchablePlaceholder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Search() {
  return (
    <RefinementList
      translations={{
-       showMore(expanded) {
-         return expanded ? 'Show less' : 'Show more';
-       },
+       showMoreButtonText({ isShowingMore }) {
+         return isShowingMore ? 'Show less' : 'Show more';
+       },
-       noResults: 'No results.',
+       noResultsText: 'No results.',
-       submitTitle: 'Submit',
+       submitButtonTitle: 'Submit',
-       resetTitle: 'Reset',
+       resetButtonTitle: 'Reset',
-       placeholder: 'Search',
      }}
+     searchablePlaceholder="Search"
    />
  );
}
Replace defaultRefinement with initialUiState on <InstantSearch>

The <RefinementList> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         query: 'iphone',
+       },
+     }}
    >
      <SearchBox
-       defaultRefinement="iphone"
      />
    </InstantSearch>
  );
}
Replace showLoadingIndicator with custom CSS

The widget now always shows a loading indicator when the search is stalled. If showLoadingIndicator is set to false in your app (or not set at all), you can hide the loading indicator with CSS.

1
2
3
.ais-SearchBox-loadingIndicator {
  display: none;
}
Replace submit with submitIconComponent, reset with resetIconComponent, and loadingIndicator with loadingIconComponent

To customize the submit, reset, and loading icons, you can use the submitIconComponent, resetIconComponent, and loadingIconComponent props. Unlike submit, reset, and loadingIndicator, these new props take React components. They give you access to the default and passed classNames.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Search() {
  return (
    <SearchBox
-     submit={<div>Submit</div>}
+     submitIconComponent={({ classNames }) => (
+       <div className={classNames.submitIcon}>Submit</div>
+     )}
-     reset={<div>Reset</div>}
+     resetIconComponent={({ classNames }) => (
+       <div className={classNames.resetIcon}>Reset</div>
+     )}
-     loadingIndicator={<div>Loading</div>}
+     loadingIconComponent={({ classNames }) => (
+       <div className={classNames.loadingIcon}>Loading</div>
+     )}
    />
  );
}
Replace focusShortcuts with custom code

The focusShortcuts prop was removed. If you want to focus the search box with custom keyboard shortcuts, you can set it up yourself with custom code.

For example, you can use react-use with the useKey Hook.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+import { useKey } from 'react-use';

 function Search() {
+  useKey('s', (event) => {
+    event.preventDefault();
+    document.querySelector('.ais-SearchBox-input').focus();
+  });

   return (
     <SearchBox
-      focusShortcuts={['s']}
     />
   );
 }
Replace translations.placeholder with placeholder

The placeholder translation now has its own top-level prop, placeholder.

1
2
3
4
5
6
7
8
9
10
function Search() {
  return (
    <SearchBox
-     translations={{
-       placeholder: 'Search here',
-     }}
+     placeholder="Search here"
    />
  );
}

<Snippet>

Replace tagName with highlightedTagName

The <Highlight> component now takes an optional highlightedTagName prop which replaces tagName.

1
2
3
4
5
6
7
8
function Search() {
  return (
    <Snippet
-     tagName="span"
+     highlightedTagName="span"
    />
  );
}

The default value also changed from "em" to "mark" for highlightedTagName (formerly tagName). If tagName is to "mark" in your app, you can remove it.

1
2
3
4
5
6
7
function Search() {
  return (
    <Snippet
-     tagName="mark"
    />
  );
}

Conversely, if you were relying on the default value for tagName, you should now set it explicitly.

1
2
3
4
5
6
7
function Search() {
  return (
    <Snippet
+     highlightedTagName="em"
    />
  );
}

<SortBy>

Replace defaultRefinement with initialUiState on <InstantSearch>

The <SortBy> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         sortBy: 'instant_search',
+       },
+     }}
    >
      <SortBy
-       defaultRefinement="instant_search"
      />
    </InstantSearch>
  );
}

<Stats>

Replace translations

The translations key have changed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Search() {
  return (
    <RefinementList
      translations={{
-       stats(nbHits, processingTimeMS, nbSortedHits, areHitsSorted) {
+       rootElementText({ nbHits, processingTimeMS, nbSortedHits, areHitsSorted }) {
          return areHitsSorted && nbHits !== nbSortedHits
          ? `${nbSortedHits!.toLocaleString()} relevant results sorted out of ${nbHits.toLocaleString()} found in ${processingTimeMS.toLocaleString()}ms`
          : `${nbHits.toLocaleString()} results found in ${processingTimeMS.toLocaleString()}ms`
        },
      }}
    />
  );
}

<ToggleRefinement>

Replace defaultRefinement with initialUiState on <InstantSearch>

The <ToggleRefinement> component no longer accepts a defaultRefinement prop. Instead, you can specify an initial UI state on your <InstantSearch> component.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App() {
  return (
    <InstantSearch
+     initialUiState={{
+       <<Your-Index-Name-Here>>: {
+         toggle: {
+           free_shipping: true
+         },
+       },
+     }}
    >
      <ToggleRefinement
        attribute="free_shipping"
-       defaultRefinement={true}
      />
    </InstantSearch>
  );
}
Replace value with on and off

By default, <ToggleRefinement> uses respectively true and undefined for new optional props on and off. You can replace the value prop with on (and off, if needed).

1
2
3
4
5
6
7
8
function Search() {
  return (
    <ToggleRefinement
-     value={'yes'}
+     on={'yes'}
    />
  );
}

Connectors

React InstantSearch v7 provides the same connectors as React InstantSearch v6 except the connectStateResults connector, which isn’t available in React InstantSearch v7.

If you were using connectors with higher-order components (HOCs), you can migrate to Hooks.

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
-import { connectSearchBox } from 'react-instantsearch-dom';
+import { useSearchBox } from 'react-instantsearch';

 function SearchBox({
- currentRefinement,
+ query,
  isSearchStalled,
  refine
 }) {
   return (
     <form noValidate action="" role="search">
       <input
         type="search"
-        value={currentRefinement}
+        value={query}
         onChange={(event) => refine(event.currentTarget.value)}
       />
       <button onClick={() => refine('')}>Reset query</button>
       {isSearchStalled ? 'My search is stalled' : ''}
     </form>
   );
 }

-const CustomSearchBox = connectSearchBox(SearchBox);

+function CustomSearchBox(props) {
+ const searchBoxApi = useSearchBox(props);

+ return <SearchBox {...searchBoxApi} />;
+}

connectStateResults()

React InstantSearch v7 doesn’t have the connectStateResults connector. You can use the useInstantSearch() hook instead.

Using connectors and Hooks

React InstantSearch v7 is a bridge between InstantSearch.js connectors and React Hooks. The connectors from React InstantSearch and InstantSearch.js were historically different APIs, meaning there are differences between the props that React InstantSearch connectors accept and the new Hooks.

The React InstantSearch v7 package uses TypeScript natively. If your editor supports code completion (IntelliSense), you can use it to discover the new props.

API reference React InstantSearch

Using Higher-Order Components (HOCs)

React InstantSearch v7 doesn’t come with Higher Order Components (HOCs). However, you can create them by wrapping existing Hooks into simple HOCs:

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
import { useSearchBox } from 'react-instantsearch';

const connectSearchBox = (Component) => {
  const SearchBox = (props) => {
    const data = useSearchBox();

    return <Component {...props} {...data} />;
  };

  return SearchBox;
};

function RawSearchBox(props) {
  const { refine, query } = props;

  return (
    <form type="search">
      <input
        type="search"
        value={query}
        onChange={(event) => refine(event.currentTarget.value)}
      />
    </form>
  );
}

const SearchBox = connectSearchBox(RawSearchBox);

You can also combine that with the useConnector() Hook to create a connector.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useConnector } from 'react-instantsearch';

const connectCustom = (Component) => {
  const Custom = (props) => {
    // myConnector.connectCustom is an InstantSearch.js connector
    const data = useConnector(myConnector.connectCustom, props);

    return <Component {...props} {...data} />;
  };

  return Custom;
};

function RawCustom(props) {
  const { refine, currentRefinement } = props;

  return (
    <button onClick={() => refine(currentRefinement)}>
      {currentRefinement}
    </button>
  );
}

const Custom = connectCustom(RawCustom);

Creating connectors

The connector API has changed to use InstantSearch.js connectors. The previous createConnector() function is no longer available.

React InstantSearch v7 works with all InstantSearch.js connectors—official Algolia connectors, and community ones.

To create your own Hook, you can use an existing connector or create your InstantSearch.js connector.

Routing

Routing now follows the InstantSearch.js routing APIs with the <InstantSearch> routing prop.

Server-side rendering (SSR)

The server APIs have been simplified in React InstantSearch v7.

Replace findResultsState() with getServerState(). This new API accepts an element <App />. You can pass props directly to <App />.

In your server code, you don’t need to provide your index name and your search client anymore, because they’re already in your <App>.

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
 import express from 'express';
 import React from 'react';
 import { renderToString } from 'react-dom/server';
-import { findResultsState } from 'react-instantsearch-dom/server';
+import { getServerState } from 'react-instantsearch';
 import App from './App';

-const indexName = 'instant_search';
-const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

 const app = express();

 app.get('/', async (req, res) => {
-  const searchState = { /* ... */ };
-  const serverState = {
-    resultsState: await findResultsState(App, {
-      searchClient,
-      indexName,
-      searchState,
-    }),
-    searchState,
-  };
+  const serverState = await getServerState(<App />);
   const html = renderToString(<App serverState={serverState} />);

   res.send(
     `
   <!DOCTYPE html>
   <html>
     <head>
       <script>window.__SERVER_STATE__ = ${JSON.stringify(serverState)};</script>
     </head>
     <body>
       <div id="root">${html}</div>
     </body>
     <script src="/assets/bundle.js"></script>
   </html>
     `
   );
 });

 app.listen(8080);

You don’t need to pass any props to <InstantSearch> to support SSR, only wrap the component to render on the server with <InstantSearchSSRProvider>. Then, pass the server state by spreading the getServerState() prop.

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
 import algoliasearch from 'algoliasearch/lite';
 import React from 'react';
-import { InstantSearch } from 'react-instantsearch-dom';
+import {
+  InstantSearch,
+  InstantSearchSSRProvider,
+} from 'react-instantsearch';

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

 function App(props) {
   return (
+    <InstantSearchSSRProvider {...props.serverState}>
       <InstantSearch
         indexName="instant_search"
         searchClient={searchClient}
-        resultsState={props.serverState.resultsState}
-        onSearchParameters={props.onSearchParameters}
-        widgetsCollector={props.widgetsCollector}
       >
         {/* Widgets */}
       </InstantSearch>
+    </InstantSearchSSRProvider>
   );
 }

 export default App;

Types

Types are now available in React InstantSearch v7.

You can uninstall all InstantSearch types coming from DefinitelyTyped.

1
npm uninstall @types/react-instantsearch-dom @types/react-instantsearch-core @types/react-instantsearch

Migrate from React InstantSearch Hooks to React InstantSearch v7

From v7, React InstantSearch Hooks is called React InstantSearch.

The migration is straightforward as the APIs are the same. Only package names have changed, and several APIs have been deprecated.

Package names

Before After
react-instantsearch-hooks react-instantsearch-core
react-instantsearch-hooks-web react-instantsearch
react-instantsearch-hooks-server react-instantsearch
react-instantsearch-hooks-router-nextjs react-instantsearch-router-nextjs

Update your package.json file:

1
2
3
4
5
6
7
8
9
10
11
{
  "dependencies": {
-   "react-instantsearch-hooks": "^6.42.0",
+   "react-instantsearch-core": "^7.0.0",
-   "react-instantsearch-hooks-web": "^6.42.0",
+   "react-instantsearch": "^7.0.0",
-   "react-instantsearch-hooks-server": "^6.42.0",
-   "react-instantsearch-hooks-router-nextjs": "^6.42.0",
+   "react-instantsearch-router-nextjs": "^7.0.0",
  }
}

After updating your package.json file, run npm install to install the updated dependencies.

You can then change your imports:

1
2
3
4
5
6
7
-import { useInstantSearch } from 'react-instantsearch-hooks';
+import { useInstantSearch } from 'react-instantsearch-core';
-import { SearchBox } from 'react-instantsearch-hooks-web';
-import { getServerState } from 'react-instantsearch-hooks-server';
+import { SearchBox, getServerState } from 'react-instantsearch';
-import { createInstantSearchRouterNext } from 'react-instantsearch-hooks-router-nextjs';
+import { createInstantSearchRouterNext } from 'react-instantsearch-router-nextjs';

Note that react-instantsearch-hooks-server functions are now included in react-instantsearch.

Codemods

To automatically update your code to use the new package names and APIs from React InstantSearch v7, Algolia provides a codemod.

First, uninstall the react-instantsearch-hooks-* packages and install their react-instantsearch-* counterparts.

1
2
npm uninstall react-instantsearch-hooks-web react-instantsearch-hooks react-instantsearch-hooks-server
npm install react-instantsearch react-instantsearch-core

Assuming your source code is in the src/ folder, run the following command:

1
npx @codeshift/cli -e js,jsx,ts,tsx --packages 'instantsearch-codemods#rish-to-ris' src/

Afterwards you should run your code formatter as codemods might output code that doesn’t follow your code style:

1
2
3
yarn prettier --write 'src/**/*.{js,jsx,ts,tsx}'
# or
yarn eslint --fix src/

use to addMiddlewares

useInstantSearch()’s use function has been renamed to addMiddlewares.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useInstantSearch } from 'react-instantsearch-core';

function Middleware() {
- const { use } = useInstantSearch();
+ const { addMiddlewares } = useInstantSearch();

  useEffect(() => {
    const middleware = /* ... */;

-   return use(middleware);
+   return addMiddlewares(middleware);
- }, [use]);
+ }, [addMiddlewares]);

  return null;
}

getServerState

The getServerState() function now requires renderToString from react-dom/server to be passed as a second argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+import { renderToString } from 'react-dom/server';
import { getServerState } from 'react-instantsearch';

function App() {
  return (
    /* ... */
  );
}

async function getServerSideProps() {
- const serverState = await getServerState(<App />);
+ const serverState = await getServerState(<App />, { renderToString });

  return {
    props: {
      serverState,
    },
  };
}

<Stats>

The translations prop of the <Stats> widget has changed and now accepts a rootElementText function instead of a stats function.

1
2
3
4
5
6
7
8
9
10
11
12
function Search() {
  return (
    <Stats
      translations={{
-       stats({ nbHits, processingTimeMS }) {
+       rootElementText({ nbHits, processingTimeMS }) {
          return `${nbHits.toLocaleString()} results found in ${processingTimeMS.toLocaleString()}ms`;
        },
      }}
    />
  );
}

Upgrade event tracking

Starting from v6.43.0, React InstantSearch Hooks simplifies the event tracking process via the insights option. You no longer need to install the search-insights library or set up the insights middleware yourself.

Here are some benefits when using the insights option:

  • It better handles the userToken. Once you set it, all the search and event tracking calls include the token.
  • It automatically sends default events from built-in widgets such as <RefinementList>, <Menu>, etc. You can also change the event payloads, or remove them altogether.
  • It lets you send custom events from your custom widgets.
  • It simplifies forwarding events to third-party trackers.

If you’ve been tracking events directly with search-insights or with the insights middleware, you should:

  1. Upgrade react-instantsearch-hooks to v6.43.0 or greater
  2. Migrate from using the insights middleware to the insights option
  3. Either update or remove the search-insights library

Use the insights option

Starting from v6.43.0, InstantSearch lets you enable event tracking with the insights option. You no longer need to set up the insights middleware yourself.

1
2
3
4
5
6
7
8
9
10
function App() {
  return (
    <InstantSearch
      // ...
      insights={true}
    >
      {/* Widgets */}
    </InstantSearch>
  );
}

If you had already set up the insights middleware in your code, you can now remove it and move its configuration to the insights option.

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
-import { createInsightsMiddleware } from 'instantsearch.js/es/middlewares';
-import { useInstantSearch } from 'react-instantsearch-hooks-web';
-import { useLayoutEffect } from 'react';
import algoliasearch from 'algoliasearch/lite';

- function InsightsMiddleware() {
-   const { addMiddlewares } = useInstantSearch();
-
-   useLayoutEffect(() => {
-     const middleware = createInsightsMiddleware({
-       insightsClient: window.aa,
-       insightsInitParams: {
-         useCookie: false,
-       }
-     });
-
-     return addMiddlewares(middleware);
-   }, [addMiddlewares]);
-
-   return null;
-}

function App() {
  return (
    <InstantSearch
      // ...
+      insights={{
+        insightsInitParams: {
+          useCookie: false,
+        }
+      }}
    >
      <InsightsMiddleware />
      {/* Widgets */}
    </InstantSearch>
  );
}

Update search-insights

Starting from v4.55.0, InstantSearch can load search-insights for you so the insightsClient option is no longer required.

If you prefer loading it yourself, make sure to update search-insights to at least v2.4.0 and forward the reference to insights.

If you’re using the UMD bundle with a <script> tag, make sure to update the full code snippet (not just the version):

1
2
3
4
5
6
7
8
<script>
  var ALGOLIA_INSIGHTS_SRC = "https://cdn.jsdelivr.net/npm/search-insights@2.13.0/dist/search-insights.min.js";

  !function(e,a,t,n,s,i,c){e.AlgoliaAnalyticsObject=s,e[s]=e[s]||function(){
  (e[s].queue=e[s].queue||[]).push(arguments)},e[s].version=(n.match(/@([^\/]+)\/?/) || [])[1],i=a.createElement(t),c=a.getElementsByTagName(t)[0],
  i.async=1,i.src=n,c.parentNode.insertBefore(i,c)
  }(window,document,"script",ALGOLIA_INSIGHTS_SRC,"aa");
</script>

If you’re using a package manager, you can upgrade it to the latest version.

1
npm install search-insights
1
2
3
4
5
6
7
8
9
10
11
12
function App() {
  return (
    <InstantSearch 
      // ...
      insights={{
        insightsClient: window.aa,
      }
    >
      {/* ... */}
    </InstantSearch>
  );
}

Otherwise, you can remove it and let InstantSearch handle it for you.

Remove search-insights

Starting from v4.55.0, InstantSearch loads search-insights for you from jsDelivr if not detected in the page. If you’ve installed search-insights, you can now remove it.

If you’re using the UMD bundle with a <script> tag, you can remove the snippet:

1
2
3
4
5
6
7
8
- <script>
-   var ALGOLIA_INSIGHTS_SRC = "https://cdn.jsdelivr.net/npm/search-insights@2.13.0/dist/search-insights.min.js";
-
-   !function(e,a,t,n,s,v,i,c){e.AlgoliaAnalyticsObject=s,e[s]=e[s]||function(){
-   (e[s].queue=e[s].queue||[]).push(arguments)},e[s].version=(n.match(/@([^\/]+)\/?/) || [])[1],i=a.createElement(t),c=a.- getElementsByTagName(t)[0],
-   i.async=1,i.src=n,c.parentNode.insertBefore(i,c)
-   }(window,document,"script",ALGOLIA_INSIGHTS_SRC,"aa");
- </script>

If you’re using a package manager, you can upgrade it to the latest version.

1
npm uninstall search-insights

Then you can remove the reference to search-insights from your code:

1
2
3
4
5
6
7
8
9
10
11
12
function App() {
  return (
    <InstantSearch 
      // ...
      insights={{
-       insightsClient: window.aa,
      }
    >
      {/* ... */}
    </InstantSearch>
  );
}

InstantSearch loads search-insights from the jsDelivr CDN, which requires that your site or app allows script execution from foreign resources. Check the security best practices for recommendations.

Did you find this page helpful?