Upgrading React InstantSearch
On this page
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 inreact-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
: usereact-instantsearch
instead.react-instantsearch-native
: usereact-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 withreact-instantsearch
. - Update most prop names and their values.
- Add
TODO
comments for the parts that need manual changes, such as replacing thedefaultRefinement
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 usingreact-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 |
<Breadcrumb>
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.
<Menu>
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"
/>
);
}
<SearchBox>
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.
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:
- Upgrade
react-instantsearch-hooks
to v6.43.0 or greater - Migrate from using the
insights
middleware to theinsights
option - 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 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.