Build a Custom Autocomplete Search Bar with React Hooks

Build a Custom Autocomplete Search Bar with React Hooks

Hello friends! I am back with another technical article for today's 6th article of the #2Articles1Week Challenge Series by Hashnode! First, I apologize for the absurdly long title. I just need to include all the juice in this article...

I hope all of you have been enjoying the challenge as much as I do! I really love reading the wonderful articles you guys are publishing! Keep it up, you are amazing!

2a1w.png

Only 2 articles left and you never need to see this cover art in the intro anymore haha...

Recently, I made an app for fun based on a game called DOTA 2 (any players here?). I used their OpenDota API to fetch some data, manipulate some stats and display it on the app. It was my first time using React Hooks so it inspired me to make this beginner-friendly version to show you how to fetch and display API data to a list using Hooks in React Native. And then have a search bar that can autocomplete a query by providing search suggestions based on that list.

Some prerequisites you need to know for this tutorial:

  • Good understanding of React Native (React is fine too)
  • Basic understanding of React Hooks
  • Good understanding of what an API is

The Simple Project

For those who don't know the game whose API we are using, don't worry. I'm simplifying the app to exclude all the stats and avoid the complicated game data for this tutorial so you can follow. Basically, we are going to:

  • Fetch all the heroes (i.e. characters) from the game using their API
  • Display their names as a FlatList in the app
  • Then, implement a Search Bar that filters the list, only displaying matched names in real-time

Okay, I hope that clears it up. Let's begin!

Step 1: Install package

First, to have a search bar in our app:

npm install react-native-elements

Step 2: Import statements and initialize states

Now we can import the SearchBar component and our Hooks: useState and useEffect. In App.js:

import { SearchBar } from 'react-native-elements';
import React, { useState, useEffect } from 'react';

Then below the App function, we initialize 3 states using useState:

  • data: an array which contains the fetched API data. Initialize as empty array.
  • query: the string value in the search bar. Initialize as empty string.
  • heroes: an array which contains the names to output to the list. Initialize as empty array.
const [data, setData] = useState([]);
const [query, setQuery] = useState('');
const [heroes, setHeroes] = useState([]);

Step 3: Fetch Data

Let's create a fetchData function to take care of fetching the API.

const fetchData = async () => {
    const res = await fetch('https://api.opendota.com/api/heroes');
    const json = await res.json();
    setData(json);
    setHeroes(json.slice());
  };

As seen in the code above, we first fetch data from the API, then set our local 'data' state to the value of the fetched data using setData(). Our heroes array will also contain the fetched data but we use .slice() to set heroes as a new array object, so that any changes to heroes will not affect the data array (as we will see later in this tutorial).

useEffect

Next, we use useEffect to call the fetchData function when the component mounts.

useEffect(() => {
    fetchData();
  }, []);

The empty array passed in as the second argument prevent fetchData() from being called every time a component updates.

The API returns an array of hero objects in this structure:

1.PNG

Step 4: Display Data to FlatList

FlatLists Props

  • data: an array that contains the elements the list will display. For this app, we will use the heroes array.
  • keyExtractor: extracts a unique key for a given item at the specified index. For this app, we will use the 'id' property of our data.
  • extraData: tells the list to re-render whenever a specified state changes. For this app, we want the list to re-render whenever the value of the query changes.
  • renderItem: takes an item from the data prop and renders it to the list. As mentioned earlier, this app only displays the name property of the heroes from the game. Thus, we add item.name instead of item inside a Text component to display the name as a text.

Here's how we implement the FlatList in our App.js:

<FlatList data={heroes} keyExtractor = {(i)=>i.id.toString()}
  extraData = {query} 
  renderItem = {({item}) =>
    <Text style={styles.flatList}>{`${item.name}`}
    </Text>} 
/>

Add some styling below:

const styles = StyleSheet.create({
  flatList:{
      paddingLeft: 15, 
      marginTop:15, 
      paddingBottom:15,
      fontSize: 20,
      borderBottomColor: '#26a69a',
      borderBottomWidth:1
  }
});

Now we should have a working app that fetches the heroes and displays their names as a list!

display.gif

But there is a problem. Apparently the API includes an npc_dota_hero_ in the beginning of every name and the names begin in small caps - looks terrible! Let's format the names so it looks nicer.

Step 5: Format Names

Create a function called formatNames that takes in a hero object. Inside the function, we will proceed in the following order:

  1. access the name property of the hero object (i.e. hero.name)
  2. remove the first 14 characters from the name (which is npc_dota_hero_)
  3. capitalize the first character of the hero's name (i.e. example_name --> Example_name)
  4. replace any '_' with a space using regex (i.e. Example_name --> Example name)
 const formatNames = (hero) => {
   let heroName = hero.name.charAt(14).toUpperCase() + hero.name.slice(15);
   heroName = heroName.replace(/_/g, " ");
   return heroName;
}

Then, we update the FlatList Text component by replacing the ${item.name} to formatNames(item).

<FlatList data={heroes} keyExtractor = {(i)=>i.id.toString()}
  extraData = {query} 
  renderItem = {({item}) =>
     <Text style={styles.flatList}>{formatNames(item)}
     </Text>} 
/>

There we go! Now the list looks so much more readable and visually nicer! Let's move on to implement our search bar to filter the list in real-time.

format.png

Search Bar Props

  • onChangeText: calls a function whenever the input text value changes. For this app, we will create a function called updateQuery to update our query state to the text value in the search bar.
  • value: the text value on the search bar. We will set it to our query state.
  • placeholder: the string the user sees on the search bar before typing on it.
    <SearchBar
     onChangeText={updateQuery}
     value={query}   
     placeholder="Type Here..."/>
    
    Our updateQuery function will take the user's input and set the query state to be equal to that value like so:
    const updateQuery = (input) => {
      setQuery(input);
      console.log(query);
    }
    
    Our console shows how our query state is updated every time there is a single character change in the search bar input.

console.PNG

Step 7: Filter Names

Just like how a Google Search suggestions work, we want the list to re-render and update in real time as the user is typing on the search bar. That way, when the user types "A", the list will immediately suggests all the names starting with A to autocomplete your query.

The extraData property in FlatList will re-render the list every time the query value changes. Therefore, as the list re-renders, we need to add some sort of filtering function to only return the names that match the query. Let's call it filterNames().

The Logic

Recall that the unformatted hero name is like: npc_dota_hero_example_name, which means we must compare the query with the name at starting from index 14, where the name actually starts.

Also, note that the user may type a query with capital letters or spaces. In some cases, this can cause formatNames to mistakenly think the query does not match the hero name.

For example, if hero name is npc_dota_hero_example_name and query is "Example NAME", it will not show a match even though it should be. We can solve this by transforming the query to all lower cases and replacing all spaces with '_' before we try to match it with our hero names.

Pseudocode

Here's the pseudocode for filterNames() to help you understand it better:

  1. Transform the query to all small caps and replace any spaces with '_'
  2. Use String.startsWith(word_to_match, start_index) to check if name starts with the query at index 14
  3. If it is a match (i.e. true), format name to its readable form by calling our formatNames() we did in Step 5.
  4. If it is not a match (i.e. false), we remove this hero element from the heroes array using splice() and do not render it to the list.

Our code would look like:

const filterNames = (hero) => {
   // 1.
   let search = query.toLowerCase().replace(/ /g,"_"); 
   //2.
   if(hero.name.startsWith(search, 14)){
      //3.
      return formatNames(hero);
   }else{ 
      //4.
      heroes.splice(heroes.indexOf(hero), 1);
      return null;
   }
}

Great! Now don't forget to update our Flatlist Text Component to call filterNames(item) instead of formatNames(item). Here's what the FlatList component should ultimately look like:

<FlatList data={heroes} keyExtractor = {(i)=>i.id.toString()}
  extraData = {query} 
  renderItem = {({item}) =>
     <Text style={styles.flatList}>{filterNames(item)}
     </Text>} 
/>

Step 8: Update the updateQuery function

Notice that we are changing the heroes array itself by removing elements that do not match the query. For example, if heroes is originally:

['Apple', 'Banana', 'Orange']

And our query is 'A', only 'Apple' would match so heroes would remove the elements that do not match. Now heroes only contains:

['Apple']

But what if the user changes the query to a name that was in the original array? Like 'O', which should match 'Orange'. Since 'Orange' has been removed, the FlatList would not display it anymore.

We can easily solve this problem by resetting the heroes array to its full length every time the user changes the query. Recall that we have a data array (see Step 2), which contains the original fetched data from the API. We never change this array so it still contains all 119 hero names. So, we can easily add:

setHeroes(data.slice());

into our updateQuery function so that the heroes array would create a new copy of the data array and therefore, contain the original length again for filterNames() to work as intended.

And ta-da! Our app should have a working search bar now!

It updates the list in real-time, providing autocomplete/search suggestions for hero names.

final.gif

Thanks for reading!

I hope this has been a fruitful read for you. Please ask any questions in the comments below. For the complete code, check out my github repo for it. Also, it's my first time to use Hooks, if there's any better solutions, please suggest it in the comments so I will learn from your feedback.

Fetching from an API, displaying it to a list and implementing a real-time search bar can be useful in many applications. I hope you'll try to make an app with your favourite game or something similar as practice!

Have fun coding, good luck and cheers!

Special Thanks to:

Did you find this article valuable?

Support Victoria Lo by becoming a sponsor. Any amount is appreciated!

ย