Concepts / Building Search UI / Native
Jul. 29, 2019

Overview

React InstantSearch is compatible with React Native.

There are no widgets for now but it’s very easy to do an application using our connectors.

You can follow our getting started tutorial or check out our more advanced React Native example linking React InstantSearch to React Native. You can also simply launch the example on your phone by using the Expo client.

Building a good React Native app will require you to learn these React InstantSearch concepts:

Getting Started with React Native InstantSearch

React Native InstantSearch is a React Native library that lets you create a mobile version of an instant search results experience using Algolia’s search API.

To get started, you will build a search UI for an e-commerce website. You will learn how to:

  • Bootstrap a React InstantSearch app with our command line utility create-instantsearch-app
  • Display and format the search bar and results
  • Use pre-built UI components (widgets) to filter results

Your goal is to create a fully working React Native InstantSearch app as fast as possible. We provide you with the data, installation instructions, and a step-by-step process with all necessary code. We will not explain how everything is wired together yet, but you’ll be able to dig into the library immediately after.

If you haven’t done so yet, take a look at our interactive getting started tutorial. It literally takes 2 minutes to complete.

⚡️ Let’s go!

Build a simple UI

Bootstrap your application

To easily bootstrap a working React Native InstantSearch app in seconds, you will use the create-instantsearch-app command line tool.

Open a terminal and paste these lines:

1
2
3
4
5
6
npx create-instantsearch-app ais-ecommerce-demo-app \
  --template "React InstantSearch Native" \
  --app-id "B1G2GM9NG0" \
  --api-key "aadef574be1f9252bb48d4ea09b5cfe5" \
  --index-name demo_ecommerce \
  --main-attribute name

This generates a folder on your machine that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
ais-ecommerce-demo-app/
├── node_modules
├── src
├── .babelrc
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── App.js
├── app.json
├── package.json
├── README.md
└── yarn.lock

Your application uses some predefined credentials (application ID, API key and index name) that we provide as part of this getting started.

create-instantsearch-app can be used to generate any flavor of InstantSearch and has many options. Read more about it on the GitHub repository.

React Native InstantSearch can be installed via an npm package in your already existing React Native application, this is covered in details in the installation guide.

Run your project

Now that we have bootstrapped the project, let’s do a first run! Inside your terminal, type:

1
2
cd ais-ecommerce-demo-app
npm start

The terminal will let you choose which simulator you want to use (Android or iOS). In this example we pick the iOS one. Once the simulator is up and ready you should see this:

Getting started react native 1

💅 You nailed it! You just bootstrapped an instant search UI in no time. Now, let’s dig into the code.

Dig in and understand the code

If you read the code of the file App.js you can see that you are using three components:

  • InstantSearch is the root React Native InstantSearch component, all other widgets need to be wrapped by this for them to function
  • SearchBox displays a nice looking SearchBox for users to type queries in it
  • InfiniteHits displays the results in a infinite list from Algolia based on the query

Using Connectors instead of Widgets

The main difference between React InstantSearch and React Native InstantSearch is that we don’t provide any widgets for React native. But you’ll still be able to build an amazing search experience using what we call connectors.

For example, let’s take a closer look at the InfiniteHits component in src/InfiniteHits.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import { StyleSheet, Text, View, FlatList } from 'react-native';
import { connectInfiniteHits } from 'react-instantsearch-native';
import Highlight from './Highlight';

const InfiniteHits = ({ hits, hasMore, refine }) => (
  <FlatList
    data={hits}
    keyExtractor={item => item.objectID}
    ItemSeparatorComponent={() => <View style={styles.separator} />}
    onEndReached={() => hasMore && refine()}
    renderItem={({ item }) => (
      <View style={styles.item}>
        <Highlight attribute="name" hit={item} />
      </View>
    )}
  />
);

// [...]

export default connectInfiniteHits(InfiniteHits);

To display results, we use the InfiniteHits connector. This connector gives you all the results returned by Algolia, and it will update when there are new results. It will also keep track of all the accumulated hits while the user is scrolling.

This connector gives you three interesting properties:

  • hits: the records that match the search state
  • hasMore: a boolean that indicates if there are more pages to load
  • refine: the function to call when the end of the page is reached to load more results.

On the React Native side, we take advantage of the FlatList to render this infinite scroll. The FlatList component is available since version v0.43 of React Native. If you’re using a previous version, you can use the ListView component instead.

That’s it! All the widgets that we are going to use with React Native follow this same pattern. Don’t hesitate to take a look at the two other widgets already implemented.

Filtering

To make your search UI more efficient and practical for your users, you will want to add a way to filter the store by brands.

Create a new file src/RefinementList.js with:

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
import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import PropTypes from 'prop-types';
import { connectRefinementList } from 'react-instantsearch-native';

const styles = StyleSheet.create({
  // fill with styles on the next step
});

const RefinementList = ({ items, refine }) => (
  <View style={styles.container}>
    <View style={styles.title}>
      <Text style={styles.titleText}>Brand</Text>
    </View>
    <View style={styles.list}>
      {items.map(item => {
        const labelStyle = {
          fontSize: 16,
          fontWeight: item.isRefined ? '800' : '400',
        };

        return (
          <TouchableOpacity
            key={item.value}
            onPress={() => refine(item.value)}
            style={styles.item}
          >
            <Text style={labelStyle}>{item.label}</Text>
            <View style={styles.itemCount}>
              <Text style={styles.itemCountText}>{item.count}</Text>
            </View>
          </TouchableOpacity>
        );
      })}
    </View>
  </View>
);

const ItemPropType = PropTypes.shape({
  value: PropTypes.arrayOf(PropTypes.string).isRequired,
  label: PropTypes.string.isRequired,
  isRefined: PropTypes.bool.isRequired,
});

RefinementList.propTypes = {
  items: PropTypes.arrayOf(ItemPropType).isRequired,
  refine: PropTypes.func.isRequired,
};

export default connectRefinementList(RefinementList);

To create this new widget we use the connector RefinementList. This widget allows us to filter our results by a given attribute.

This connector gives you two interesting properties:

  • items: the list of refinements
  • refine: the function to call when a new category is selected

Then you can add this new widget to your App component:

1
2
3
4
5
6
7
8
9
10
11
12
13
import RefinementList from './src/RefinementList';

// [...]

<InstantSearch
  searchClient={searchClient}
  indexName="demo_ecommerce"
  root={this.root}
>
  <SearchBox />
  <RefinementList attribute="brand" limit={5} />
  <InfiniteHits />
</InstantSearch>

The attribute props specifies the faceted attribute to use in this widget. This attribute should be declared as a facet in the index configuration as well. Here we are using the brand attribute. Note that we are also using another prop limit. It’s not required, but it ensures that the list is not too long depending on which simulator you are.

You can polish the UI with these styles in src/RefinementList.js:

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
const styles = StyleSheet.create({
  container: {
    padding: 10,
    backgroundColor: '#FFFFFF',
  },
  title: {
    alignItems: 'center',
  },
  titleText: {
    fontSize: 20,
  },
  list: {
    marginTop: 20,
  },
  item: {
    paddingVertical: 10,
    flexDirection: 'row',
    justifyContent: 'space-between',
    borderBottomWidth: 1,
    alignItems: 'center',
  },
  itemCount: {
    backgroundColor: '#252b33',
    borderRadius: 25,
    paddingVertical: 5,
    paddingHorizontal: 7.5,
  },
  itemCountText: {
    color: '#FFFFFF',
    fontWeight: '800',
  },
});

Go to your simulator, it has reloaded and now you can see this:

Getting started react native 2

🍬 Sweet! You just added a new widget to your first instant-search page.

Create a modal

We don’t have that much space available on our mobile screen so we’re going to extract this RefinementList and display it inside a Modal.

When using the Modal, components that are displayed inside it are mounted and unmounted depending on whether the Modal is visible or not. One thing to know about React InstantSearch is that a refinement is applied only if its corresponding widget is mounted. If a widget is unmounted then its state is removed from the search state as well.

To keep track of the refinements applied inside the Modal (or another screen if you have navigation), you’ll need to use another InstantSearch component and synchronize the search state between them.

InstantSearch takes two interesting props:

  • onSearchStateChange: a function that is called every time the search state changes.
  • searchState: the search state to apply.

We are going to leverage those two props to keep in sync our two InstantSearch components.

First, let’s create the Modal in a new file src/Filters.js:

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
import React from 'react';
import {
  StyleSheet,
  SafeAreaView,
  Modal,
  Text,
  TouchableOpacity,
} from 'react-native';
import PropTypes from 'prop-types';
import { InstantSearch } from 'react-instantsearch-native';
import RefinementList from './RefinementList';

const styles = StyleSheet.create({
  closeButton: {
    alignItems: 'center',
    marginTop: 20,
  },
  closeButtonText: {
    fontSize: 18,
  },
});

const Filters = ({
  isModalOpen,
  searchState,
  searchClient,
  toggleModal,
  onSearchStateChange,
}) => (
  <Modal animationType="slide" visible={isModalOpen}>
    <SafeAreaView>
      <InstantSearch
        searchClient={searchClient}
        indexName="demo_ecommerce"
        searchState={searchState}
        onSearchStateChange={onSearchStateChange}
      >
        <RefinementList attribute="brand" />
        <TouchableOpacity style={styles.closeButton} onPress={toggleModal}>
          <Text style={styles.closeButtonText}>Close</Text>
        </TouchableOpacity>
      </InstantSearch>
    </SafeAreaView>
  </Modal>
);

Filters.propTypes = {
  isModalOpen: PropTypes.bool.isRequired,
  searchState: PropTypes.object.isRequired,
  searchClient: PropTypes.object.isRequired,
  toggleModal: PropTypes.func.isRequired,
  onSearchStateChange: PropTypes.func.isRequired,
};

export default Filters;

Second, let’s add our Modal to our first App 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
import { StyleSheet, View, SafeAreaView, StatusBar, Button } from 'react-native';
import Filters from './src/Filters';

// [...]

class App extends React.Component {
  root = {
    Root: View,
    props: {
      style: {
        flex: 1,
      },
    },
  };

  state = {
    isModalOpen: false,
    searchState: {},
  };

  toggleModal = () =>
    this.setState(({ isModalOpen }) => ({
      isModalOpen: !isModalOpen,
    }));

  onSearchStateChange = searchState =>
    this.setState(() => ({
      searchState,
    }));

  render() {
    const { isModalOpen, searchState } = this.state;

    return (
      <SafeAreaView style={styles.safe}>
        <StatusBar barStyle="light-content" />
        <View style={styles.container}>
          <InstantSearch
            searchClient={searchClient}
            indexName="demo_ecommerce"
            root={this.root}
            searchState={searchState}
            onSearchStateChange={this.onSearchStateChange}
          >
            <Filters
              isModalOpen={isModalOpen}
              searchClient={searchClient}
              searchState={searchState}
              toggleModal={this.toggleModal}
              onSearchStateChange={this.onSearchStateChange}
            />

            <SearchBox />
            <Button
              title="Filters"
              color="#252b33"
              onPress={this.toggleModal}
            />
            <InfiniteHits />
          </InstantSearch>
        </View>
      </SafeAreaView>
    );
  }
}

Go to your simulator, it has reloaded and now you can see this:

Getting started react native 3

If you tried the application and selected a brand, you saw that when you closed the Modal, the refinement is not applied to the search. This is because for a refinement to be applied, it needs to be present inside the search state and have a corresponding widget mounted.

So indeed, we will need a RefinementList mounted on our first InstantSearch. Of course, we don’t want to render anything, we just want to apply the refinement.

Luckily we can leverage a concept that is called Virtual Widgets. Those widgets allow you to pre-refine any widget without rendering anything.

Let’s create one for our RefinementList in App.js:

1
2
3
4
5
import { InstantSearch, connectRefinementList } from 'react-instantsearch-native';

// [...]

const VirtualRefinementList = connectRefinementList(() => null);

Let’s add it to our 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
// [...]

<SafeAreaView style={styles.safe}>
  <StatusBar barStyle="light-content" />
  <View style={styles.container}>
    <InstantSearch
      searchClient={searchClient}
      indexName="demo_ecommerce"
      root={this.root}
      searchState={searchState}
      onSearchStateChange={this.onSearchStateChange}
    >
      <VirtualRefinementList attribute="brand" />
      <Filters
        isModalOpen={isModalOpen}
        searchClient={searchClient}
        toggleModal={this.toggleModal}
        searchState={searchState}
        onSearchStateChange={this.onSearchStateChange}
      />
      <SearchBox />
      <Button
        title="Filters"
        color="#252b33"
        onPress={this.toggleModal}
      />
      <InfiniteHits />
    </InstantSearch>
  </View>
</SafeAreaView>

Congrats, you can now filter your results by brands!

That’s it!

You just learned how to create your first React Native InstantSearch application. You can find the source code of this example on GitHub.

Did you find this page helpful?