Guides / Building Search UI / Going further / Native

Use React InstantSearch Hooks with React Native

React InstantSearch Hooks 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 start

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-hooks.

1
2
3
npm install algoliasearch react-instantsearch-hooks
# or
yarn add algoliasearch react-instantsearch-hooks

Add InstantSearch to your app

The root component to start using React InstantSearch Hooks 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, View } from 'react-native';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-hooks';

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.

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 Hooks 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
55
56
57
58
59
import React, { useEffect, useRef, useState } from 'react';
import { StyleSheet, View, TextInput } from 'react-native';
import { useSearchBox } from 'react-instantsearch-hooks';

export function SearchBox(props) {
  const { query, refine } = useSearchBox(props);
  const [value, setValue] = useState(query);
  const inputRef = useRef(null);

  // Track when the value coming from the React state changes to synchronize
  // it with InstantSearch.
  useEffect(() => {
    if (query !== value) {
      refine(value);
    }
  }, [value, refine]);

  // Track when the InstantSearch query changes to synchronize it with
  // the React state.
  useEffect(() => {
    // We bypass the state update if the input is focused to avoid concurrent
    // updates when typing.
    if (!inputRef.current?.isFocused() && query !== value) {
      setValue(query);
    }
  }, [query]);

  return (
    <View style={styles.container}>
      <TextInput
        ref={inputRef}
        style={styles.input}
        value={value}
        onChangeText={setValue}
        clearButtonMode="while-editing"
        autoCapitalize="none"
        autoCorrect={false}
        spellCheck={false}
        autoCompleteType="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
import React from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import { useInfiniteHits } from 'react-instantsearch-hooks';

export function InfiniteHits({ hitComponent: Hit, ...props }) {
  const { hits, isLastPage, showMore } = useInfiniteHits(props);

  return (
    <FlatList
      data={hits}
      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.

Loading more hits as you scroll

Loading more hits as you scroll

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. Make sure to install it before using it in your app.

1
2
3
npm install instantsearch.js
# or
yarn add instantsearch.js

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.

Search results for the query apple

Search results for the query “apple”

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-hooks';

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.

Filters in a modal

Filters in a modal

Next steps

You now have a good starting point to create an even more dynamic experience with React InstantSearch Hooks and React Native. Next up, you could improve this app by:

Did you find this page helpful?