Build a Custom Autocomplete Search Bar with React Hooks
Tags: reactnative, frontend, api, 2articles1week
Published: 2020-12-29 03:17:03

---
title: Build a Custom Autocomplete Search Bar with React Hooks
published: true
date: 2020-07-23 23:11:38 UTC
tags: ReactNative,FrontendDevelopment,APIs,2Articles1Week
canonical_url: https://lo-victoria.com/build-a-custom-autocomplete-search-bar-with-react-hooks
---

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](https://docs.opendota.com/)
- Display their names as a [FlatList](https://reactnative.dev/docs/flatlist) in the app
- Then, implement a [Search Bar](https://react-native-elements.github.io/react-native-elements/docs/searchbar.html) 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](https://react-native-elements.github.io/react-native-elements/docs/searchbar.html) 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1594822060951/UgBktjPqh.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](https://cdn.hashnode.com/res/hashnode/image/upload/v1594830027035/mQGRSpWQt.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](https://cdn.hashnode.com/res/hashnode/image/upload/v1594831155718/CeAPpFLW6.png)


### Step 6: Search Bar
#### 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1594832170570/ctkYZK3Qu.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](https://cdn.hashnode.com/res/hashnode/image/upload/v1594836108688/PjsKiI18j.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](https://github.com/victoria-lo/DOTAHeroes) 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:
- Rana Emad for her article on [React Hooks](https://blog.ranaemad.com/whiteboard-react-hooks-ckclrvccg0005fls16f1h80mc)
- Steffy Lo for her article on [Introduction to React Hooks](https://steffy.hashnode.dev/introduction-to-react-hooks-cka9yy0wj0118c5s1xp9gtfwj)
- [FlatList Documentation](https://reactnative.dev/docs/flatlist) from React Native 
- [SearchBar Documentation](https://react-native-elements.github.io/react-native-elements/docs/searchbar.html) from React Native Elements
- [OpenDota API](https://docs.opendota.com/) for inspiring me