Search with Algolia in React Native using React InstantSearch (using connectors instead of widgets)

What is Algolia?

Algolia is a hosted search engine, offering full-text, numerical, and faceted search, capable of delivering real-time results from the first keystroke. Algolia’s powerful API lets you quickly and seamlessly implement search within your websites and mobile applications. It's search API powers billions of queries for thousands of companies every month, delivering relevant results in under 100ms anywhere in the world. Read More

Algolia cares deeply about open source. It maintain open source libraries such as InstantSearch libraries for web and mobile to build rich user experiences, and engage with the OpenCollective community to help finance open source projects.

What Is InstantSearch?

The Perfect Match : Libraries built to unleash the full potential of Algolia’s search infrastructure.
InstantSearch is a family of open-source, production-ready UI libraries that eases the usage and installation of the Algolia search engine. It provides high-level UI widgets that interact with Algolia’s API, to easily build instant-search applications, where you focus on building your UI instead of needing to understand every detail of the Algolia search engine right away

The InstantSearch family is composed of multiple InstantSearch flavours which are following : React InstantSearch | InstantSearch.js | Angular InstantSearch | Vue InstantSearch | InstantSearch Android | InstantSearch iOS

Note: React InstantSearch is compatible with React Native. However, React InstantSearch widgets are not compatible with React Native. To leverage InstantSearch features in React Native, you must use connectors.

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.

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

  • Connectors : The connector handles the business logic and exposes a simplified API to the rendering function (items, refine, widget parameters). In this case, it receives a function to change the currently selected value and the list of options to display. Most of the connectors will use the same naming for properties passed down to your components.
    items[]: array of items to display, for example the brands list of a custom Refinement List. Every extended widget displaying a list gets an items property to the data passed to its render function.
    refine(value|item.value): will refine the current state of the widget.
    currentRefinement: currently applied refinement value (usually the call value of refine()).
    createURL(value|item.value): will return a full url you can display for the specific refine value given you are using the routing feature.
  • Virtual widgets : React InstantSearch widgets are built in two parts:
     - business logic code
       - rendering code
    The business logic is what we call connectors. They are implemented with higher order components. They encapsulate the logic for a specific kind of widget and they provide a way to interact with the InstantSearch context. Those connectors allow you to completely extend existing widgets. The rendering is the React specific code that is tied to each platform (DOM or Native). The state of all widgets is exposed in the searchState object. You can learn more about it in the dedicated section searchState.
    Note: To see all available widgets, check out the widgets list, and To see the widgets in action, check out our interactive widget showcase.
  • Routing : React InstantSearch provides the necessary API to synchronize the state of your search UI (e.g., refined widgets, current search query) with any kind of storage. This is possible with the props available on the InstantSearch component.
    Routing API Overview  

    - searchState This object describes the current state of the search. It contains the states of all the widgets and turns the InstantSearch component into a controlled component. You can learn more on the search state guide
    - onSearchStateChange(nextSearchState) This function is called every time the search state is updated.
    - createURL(searchState)his function returns a string defining the URL based on all widgets generating parameters and passed down to every connector.
Note: You can follow algolia 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. create-instantsearch-app can be used to generate any flavor of InstantSearch and has many options. Read more about it on the GitHub repository.

In this article I'm going to explain how to integrate algolia's instant search in react-native using Connectors and create custom widgets. I have created demo example for reference you can get it from here GitHub Repository AlgoliaSearchDemo

Requirement For Setup

  1. Create the react-native project

        react-native init <project-name>
    
  2. Install React Native version of Algolia's instantsearch library "react-instantsearch-native"

        npm install --save react-instantsearch-native
    
  3. Having some predefined credentials (application ID, API key and index name) that we provide as part of this getting started. I have used algolia's default test case account configs:

    Application Id : B1G2GM9NG0
    API Key : aadef574be1f9252bb48d4ea09b5cfe5
    indexName : demo_ecommerce

Run your project

Now that we have bootstrapped the project by Inside your terminal, type:

    react-native run-ios  or react-native run-android

Here we pick the iOS one. Once the simulator is up and ready you should see this:

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
// [...]
import { InstantSearch } from 'react-instantsearch-native';
import SearchBox from './src/SearchBox';
import InfiniteHits from './src/InfiniteHits';
// [...]

<InstantSearch
    appId="B1G2GM9NG0"
    apiKey="aadef574be1f9252bb48d4ea09b5cfe5" 
    indexName="demo_ecommerce"
    root={this.root}
  >
    <SearchBox />
    <InfiniteHits />
    // import more custom widgets
  </InstantSearch>
</View>

Using Connectors instead of Widgets

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

Custom Widgets are generally created in three steps:

  1. Create a React component
  2. Connect the component using the connector
  3. Use your connected widget

For example, To create custom SearchBox widget using connector as following:

// 1. Create a React component
const SearchBox = () => {
  // return the DOM output
};

// 2. Connect the component using the connector
const CustomSearchBox = connectSearchBox(SearchBox);

// 3. Use your connected widget
<CustomSearchBox />

In App.js file you can see that we used SearchBox custom widget component in src/SearchBox.js:

import React from 'react';
import { StyleSheet, View, TextInput } from 'react-native';
import PropTypes from 'prop-types';
import { connectSearchBox } from 'react-instantsearch-native';

const styles = StyleSheet.create({
  container: {
    padding: 16,
    backgroundColor: '#000000'
  },
  input: {
    height: 48,
    padding: 12,
    fontSize: 16,
    backgroundColor: '#fff',
    borderRadius: 4,
    borderWidth: 1,
    borderColor: '#ddd',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 2,
  },
});

const SearchBox = ({ currentRefinement, refine }) => {    
  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        onChangeText={value => refine(value)}
        value={currentRefinement}
        placeholder=""
      />
    </View>
  )
};

SearchBox.propTypes = {
  currentRefinement: PropTypes.string.isRequired,
  refine: PropTypes.func.isRequired,
};

export default connectSearchBox(SearchBox);

Here you can see how the InfiniteHits component in src/InfiniteHits.js:

import React from 'react';
import { StyleSheet, Text, View, FlatList } from 'react-native';
import Highlight from './Highlight'
import PropTypes from 'prop-types';
import { connectInfiniteHits } from 'react-instantsearch-native';

const styles = StyleSheet.create({
  separator: {
    borderBottomWidth: 1,
    borderColor: '#ddd',
  },
  item: {
    padding: 10,
    flexDirection: 'column',
  },
  titleText: {
    fontWeight: 'bold',
  },
});

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>
    )}
  />
);

InfiniteHits.propTypes = {
  hits: PropTypes.arrayOf(PropTypes.object).isRequired,
  hasMore: PropTypes.bool.isRequired,
  refine: PropTypes.func.isRequired,
};

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.

In InfinitHits component, We used HighLight Component as a child component which provide the highlight color on the matching hits in InfinitHits result. you can see how Highlight component look like:

import React from 'react';
import { Text } from 'react-native';
import PropTypes from 'prop-types';
import { connectHighlight } from 'react-instantsearch-native';

const Highlight = ({ attribute, hit, highlight }) => {
  const highlights = highlight({
    highlightProperty: '_highlightResult',
    attribute,
    hit,
  });
  return (
    <Text>
      {highlights.map(({ value, isHighlighted }, index) => {
        const style = {
          backgroundColor: isHighlighted ? 'yellow' : 'transparent',
        };

        return (
          <Text key={index} style={style}>
            {value}
          </Text>
        );
      })}
    </Text>
  );
};

Highlight.propTypes = {
  attribute: PropTypes.string.isRequired,
  hit: PropTypes.object.isRequired,
  highlight: PropTypes.func.isRequired,
};

export default connectHighlight(Highlight);

Filtering

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

Create a new file src/RefinementList.js with:

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({
  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',
  },
});


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.js component:

import RefinementList from './src/RefinementList';

// [...]

<InstantSearch
  appId="B1G2GM9NG0"
  apiKey="aadef574be1f9252bb48d4ea09b5cfe5
  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.

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

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

Note: We don’t have that much space available on our mobile screen so we have to extract this RefinementList and display it inside a Modal or other page. At that time require anohter Instantsearch component. So, how to use more than one InstantSearch component and synchronize the search state between them will explain in NEXT BLOG. Thank You :-)