Use React InstantSearch with React Native
This is the React InstantSearch v7 documentation. React InstantSearch v7 is the latest version of React InstantSearch and the stable version of React InstantSearch Hooks.
If you were using React InstantSearch v6, you can upgrade to v7.
If you were using React InstantSearch Hooks, you can still use the React InstantSearch v7 documentation, but you should check the upgrade guide for necessary changes.
If you want to keep using React InstantSearch v6, you can find the archived documentation.
On this page
React InstantSearch is compatible with React Native. The library provides pre-built, web-based UI components which aren’t compatible with React Native, but you can use Hooks with the React Native core components or any third-party React Native component library.
This guide covers how to integrate InstantSearch in your React Native application:
- Adding a search box to send queries
- Displaying infinite hits and highlighting matches
- Filtering in a modal to narrow down the results set
Before you begin
This guide assumes React and React Native knowledge, and an existing React Native app using React ≥ 16.8.0. If you don’t already have a running React Native app, you can set one up following the React Native documentation.
Then, install algoliasearch
and react-instantsearch-core
.
1
2
3
npm install algoliasearch react-instantsearch-core
# or
yarn add algoliasearch react-instantsearch-core
Add InstantSearch to your app
The root component to start using React InstantSearch is the <InstantSearch>
provider. This component connects your InstantSearch app to your Algolia application. It accepts a search client and the index to search into.
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
import React from 'react';
import { SafeAreaView, StatusBar, StyleSheet, Text, View } from 'react-native';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-core';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
export default function App() {
return (
<SafeAreaView style={styles.safe}>
<StatusBar style="light" />
<View style={styles.container}>
<InstantSearch searchClient={searchClient} indexName="instant_search">
{/* ... */}
</InstantSearch>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: '#252b33',
},
container: {
flex: 1,
backgroundColor: '#ffffff',
flexDirection: 'column',
},
});
You can add some styles with the StyleSheet
API from React Native.
Add a search box
The main UI component of a search experience is a search box. It’s usually your users’ entry point to discover the content of the app.
React InstantSearch provides a useSearchBox()
Hook to build a search box connected to Algolia. You can use it together with the <TextInput>
React Native component.
Then, you can add the custom <SearchBox>
component to 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
43
44
45
46
47
48
49
50
51
52
53
54
import React, { useRef, useState } from 'react';
import { StyleSheet, View, TextInput } from 'react-native';
import { useSearchBox } from 'react-instantsearch-core';
export function SearchBox(props) {
const { query, refine } = useSearchBox(props);
const [inputValue, setInputValue] = useState(query);
const inputRef = useRef(null);
function setQuery(newQuery) {
setInputValue(newQuery);
refine(newQuery);
}
// Track when the InstantSearch query changes to synchronize it with
// the React state.
// We bypass the state update if the input is focused to avoid concurrent
// updates when typing.
if (query !== inputValue && !inputRef.current?.isFocused()) {
setInputValue(query);
}
return (
<View style={styles.container}>
<TextInput
ref={inputRef}
style={styles.input}
value={inputValue}
onChangeText={setQuery}
clearButtonMode="while-editing"
autoCapitalize="none"
autoCorrect={false}
spellCheck={false}
autoComplete="off"
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#252b33',
padding: 18,
},
input: {
height: 48,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
borderRadius: 4,
borderWidth: 1,
borderColor: '#ddd',
},
});
Display infinite hits
When Algolia returns new results, you want to list them in the UI. A common pattern when dealing with lists on mobile is to display an infinite list that loads more results when scrolling to the bottom of the screen.
You can use the useInfiniteHits()
Hook along with <FlatList>
from React Native to display Algolia hits and load more when you reach the end. Then, you can add the custom <InfiniteHits>
component in your app and pass it a custom <Hit>
component to display each Algolia hit.
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
import React from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import { useInfiniteHits } from 'react-instantsearch-core';
export function InfiniteHits({ hitComponent: Hit, ...props }) {
const { items, isLastPage, showMore } = useInfiniteHits({
...props,
escapeHTML: false,
});
return (
<FlatList
data={items}
keyExtractor={(item) => item.objectID}
ItemSeparatorComponent={() => <View style={styles.separator} />}
onEndReached={() => {
if (!isLastPage) {
showMore();
}
}}
renderItem={({ item }) => (
<View style={styles.item}>
<Hit hit={item} />
</View>
)}
/>
);
};
const styles = StyleSheet.create({
separator: {
borderBottomWidth: 1,
borderColor: '#ddd',
},
item: {
padding: 18,
},
});
Now, whenever you type a query, the list updates with search results from Algolia. You can scroll down to load more hits.
Note that the search box remains visible when you scroll, so you can update your query without manually scrolling back up. When you do, you need to make sure to reset the scroll position of the list so you can see the top results first.
To do so, you can pass a ref to the list to imperatively use its APIs higher up in the component tree. Wrap the <InfiniteHits>
component with forwardRef
to pass down the ref from the app, then accept an onChange
callback on the custom <SearchBox>
to call FlatList.scrollToOffset()
when the query changes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { forwardRef } from 'react';
// ...
export const InfiniteHits = forwardRef(
({ hitComponent: Hit, ...props }, ref) => {
// ...
return (
<FlatList
ref={ref}
// ...
/>
);
}
);
// ...
Highlight matches
Algolia supports highlighting and returns the highlighted parts of a hit in the response. You can build a custom <Highlight>
component to highlight matches in each attribute.
The instantsearch.js
library provides the necessary utilities to parse the highlighted parts from the Algolia response. Because React InstantSearch uses InstantSearch.js under the hood, you don’t need to explicitly add it to your list of dependencies.
You can use <Text>
from React Native to represent each highlighted and non-highlighted part. Then, you can use the custom <Highlight>
in your custom <Hit>
component.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import React, { Fragment } from 'react';
import { StyleSheet, Text } from 'react-native';
import {
getHighlightedParts,
getPropertyByPath,
} from 'instantsearch.js/es/lib/utils';
function HighlightPart({ children, isHighlighted }) {
return (
<Text style={isHighlighted ? styles.highlighted : styles.nonHighlighted}>
{children}
</Text>
);
}
export function Highlight({ hit, attribute, separator = ', ' }) {
const { value: attributeValue = '' } =
getPropertyByPath(hit._highlightResult, attribute) || {};
const parts = getHighlightedParts(attributeValue);
return (
<>
{parts.map((part, partIndex) => {
if (Array.isArray(part)) {
const isLastPart = partIndex === parts.length - 1;
return (
<Fragment key={partIndex}>
{part.map((subPart, subPartIndex) => (
<HighlightPart
key={subPartIndex}
isHighlighted={subPart.isHighlighted}
>
{subPart.value}
</HighlightPart>
))}
{!isLastPart && separator}
</Fragment>
);
}
return (
<HighlightPart key={partIndex} isHighlighted={part.isHighlighted}>
{part.value}
</HighlightPart>
);
})}
</>
);
}
const styles = StyleSheet.create({
highlighted: {
fontWeight: 'bold',
backgroundColor: '#f5df4d',
color: '#6f6106',
},
nonHighlighted: {
fontWeight: 'normal',
backgroundColor: 'transparent',
color: 'black',
},
});
Now when you type a query, the UI highlights matches in each search result.
Filter with a refinement list in a modal
A search box is a great way to refine a search experience, but sometimes you need to narrow down the results to find what you’re looking for in a specific category. You can use useRefinementList()
to filter items based on their brand, size, color, etc.
Mobile devices have limited screen real estate. Instead of displaying filters near the list of hits as you would on a desktop website, mobile apps commonly set filters inside a modal. With React Native, you can use the <Modal>
component.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import React from 'react';
import {
Button,
StyleSheet,
SafeAreaView,
Modal,
Text,
TouchableOpacity,
View,
} from 'react-native';
import {
useClearRefinements,
useCurrentRefinements,
useRefinementList,
} from 'react-instantsearch-core';
export function Filters({ isModalOpen, onToggleModal, onChange }) {
const { items, refine } = useRefinementList({ attribute: 'brand' });
const { canRefine: canClear, refine: clear } = useClearRefinements();
const { items: currentRefinements } = useCurrentRefinements();
const totalRefinements = currentRefinements.reduce(
(acc, { refinements }) => acc + refinements.length,
0
);
return (
<>
<TouchableOpacity style={styles.filtersButton} onPress={onToggleModal}>
<Text style={styles.filtersButtonText}>Filters</Text>
{totalRefinements > 0 && (
<View style={styles.itemCount}>
<Text style={styles.itemCountText}>{totalRefinements}</Text>
</View>
)}
</TouchableOpacity>
<Modal animationType="slide" visible={isModalOpen}>
<SafeAreaView>
<View style={styles.container}>
<View style={styles.title}>
<Text style={styles.titleText}>Brand</Text>
</View>
<View style={styles.list}>
{items.map((item) => {
return (
<TouchableOpacity
key={item.value}
style={styles.item}
onPress={() => {
refine(item.value);
onChange();
}}
>
<Text
style={{
...styles.labelText,
fontWeight: item.isRefined ? '800' : '400',
}}
>
{item.label}
</Text>
<View style={styles.itemCount}>
<Text style={styles.itemCountText}>{item.count}</Text>
</View>
</TouchableOpacity>
);
})}
</View>
</View>
<View style={styles.filterListButtonContainer}>
<View style={styles.filterListButton}>
<Button
title="Clear all"
color="#252b33"
disabled={!canClear}
onPress={() => {
clear();
onChange();
onToggleModal();
}}
/>
</View>
<View style={styles.filterListButton}>
<Button
onPress={onToggleModal}
title="See results"
color="#252b33"
/>
</View>
</View>
</SafeAreaView>
</Modal>
</>
);
}
const styles = StyleSheet.create({
container: {
padding: 18,
backgroundColor: '#ffffff',
},
title: {
alignItems: 'center',
},
titleText: {
fontSize: 32,
},
list: {
marginTop: 32,
},
item: {
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderColor: '#ddd',
alignItems: 'center',
},
itemCount: {
backgroundColor: '#252b33',
borderRadius: 24,
paddingVertical: 4,
paddingHorizontal: 8,
marginLeft: 4,
},
itemCountText: {
color: '#ffffff',
fontWeight: '800',
},
labelText: {
fontSize: 16,
},
filterListButtonContainer: {
flexDirection: 'row',
},
filterListButton: {
flex: 1,
alignItems: 'center',
marginTop: 18,
},
filtersButton: {
paddingVertical: 18,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
filtersButtonText: {
fontSize: 18,
textAlign: 'center',
},
});
You get a Filters button to reveal filters in a modal that slides from the bottom.
The filters modal also uses the useCurrentRefinements()
and useClearRefinements()
Hooks to display how many refinements are applied and let you clear them all at once.
Next steps
You now have a good starting point to create an even more dynamic experience with React InstantSearch and React Native. Next up, you could improve this app by:
- Searching in multiple indices with
<Index>
. - Using a different component library like Lottie React Native or React Native Elements.
- Checking the API reference to discover more Hooks.