React Native Searchable FlatList with React Hooks

If you have been using a mobile app or building one, you must be very familiar with the content of a very long list such as merchandise products, contacts, friends, countries, etc. It is very inconvenient for the user to find the required record by scrolling the whole list. Most of the mobile apps will provide a search bar for the user to search for their desire record for better user experience.

In React Native, FlatList is the common component that you will use to build the long list. FlatList can render the long and performant list just with 2 basic props: renderItem and data. If you wish to use sections, you may use <a rel="noreferrer noopener" aria-label="SectionList (opens in a new tab)" href="https://facebook.github.io/react-native/docs/sectionlist" target="_blank">SectionList</a>.

For this tutorial, we will build a basic searchable country list similar to the GIF below and the solution will be demonstrated using React Hooks.

Country Selection List from Spendie Mobile App
Country Selection List in Spendie (iOS & Android)

Without any further ado, let’s get started!

Building The List

First, let’s build the UI of a search bar and a simple list of countries.

import React, { useState } from 'react';
import { View, Text, FlatList } from 'react-native';
import { SearchBar } from 'react-native-elements';
import { countries } from 'countries-list';

// Get the values of the countries and sort is alphabetically
const countryList = Object.values(countries)
  .map((country) => ({
    ...country,
    lowerCaseName: country.name.toLowerCase(),
  }))
  .sort((a, b) => a.name > b.name);

const SearchableFlatList = () => {
  const [query, setQuery] = useState('');
  const [filteredCountryList, setFilteredCountryList] = useState(countryList);

  return (
    <View>
      <SearchBar
        placeholder="Search your countries..."
        onChangeText={setQuery}
        value={query}
      />
      <FlatList
        keyExtractor={(item, index) => `${index}`}
        data={filteredCountryList}
        renderItem={({ item }) => <Text>{item.name}</Text>}
      />
    </View>
  );
};

export default SearchableFlatList;

From the code snippet above, we are using open source <a rel="noreferrer noopener" aria-label="countries-list (opens in a new tab)" href="https://github.com/annexare/Countries" target="_blank">countries-list</a> to get the list of countries. Due to countries is a key-value object, thus, we need to use Object.values to get the array of the country. You must be curious why we need to add lowerCaseName for each country. The reason is that we may want to ignore the cases when we search the list. Thus, by adding the lowerCaseName, we can use it to make our search case-insensitive. Last but not least, we also sort them alphabetically according to the name.

Then, we create a state filteredCountries for holding the filtered country list and set the initial value to the countryList. Next, assign the filteredCountries to the FlatList for rendering.

For the search bar, we can use awesome SearchBar component from <a rel="noreferrer noopener" aria-label="react-native-elements (opens in a new tab)" href="https://react-native-elements.github.io/react-native-elements/docs/getting_started.html" target="_blank">react-native-elements</a>. After that, we create another state query and set it when user changes the input of the search bar.

Until now, we have a search bar and a list of countries display on the screen. However, there is still no logic to search the country list when the search bar’s value is changed. Let’s add the search logic now.

Search the List

We have all the UI set and we can add the search logic now.

useEffect(() => {
  const lowerCaseQuery = query.toLowerCase();
  const newCountries = countryList
    .filter((country) => country.lowerCaseName.includes(lowerCaseQuery));

  setFilteredCountryList(newCountries);
}, [query]);

Now, we will use the effect hook and put query as the dependency of the hook. By setting the dependency, the effect hook will only be called when the query is changed. Similar to what we did previously, we will create a lower case version of the query so that we can compare with the lower case version of the country’s name. We use includes to filter the country that contains the query in its name. Don’t forget to import useEffect in your file!

Now, you can run your React Native app and you should able to search the country list when you type in the search bar.

Sorting the Result

Sometimes, for better user experience, you may want to sort the result based on the search queries. For example, if user type o in the search box, you want to display the country’s name that start with o at the top of the result. To achieve this, we can rank our result based on the indexOf the query in the country’s name and sort them accordingly.

useEffect(() => {
  const lowerCaseQuery = query.toLowerCase();
  const newCountries = countryList
    .filter((country) => country.lowerCaseName.includes(lowerCaseQuery))
    .map((country) => ({
      ...country,
      rank: country.lowerCaseName.indexOf(lowerCaseQuery),
    }))
    .sort((a, b) => a.rank - b.rank);

  setFilteredCountryList(newCountries);
}, [query]);

Using Debounce

Besides ranking the result, there is another enhancement we can do for the search, It is not wise if we try to search the result while the user is still typing because you may need to do a lot of processing and may create flickering on the screen. If you are calling API for the filter/search, you will have a lot of redundant calls and handling for race condition. To fix the problem easily, we can use debounce to wait until the typing action is finished before we proceed to search the result. It will create a more performant and responsive experience for the user. To understand more about debounce, you may read this tutorial.

Let’s create a simple debounce hook.

const useDebounce = (value: any, delay: number) => {
  const [debounceValue, setDebounceValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounceValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debounceValue;
};

Next, we will debounce the query for 300 milliseconds. It will gives the user a 300 milliseconds window to type the next character before we start to search the result.

const debounceQuery = useDebounce(query, 300);

Lastly, we change the effect hook’s dependencies from query to debounceQuery.

useEffect(() => {
  const lowerCaseQuery = debounceQuery.toLowerCase();
  ...
}, [debounceQuery]);

By adding the debounce, we can save some processing power when user is typing in the search bar. This is the final code snippet for this tutorial.

import React, { useState, useEffect } from 'react';
import { View, Text, FlatList } from 'react-native';
import { SearchBar } from 'react-native-elements';
import { countries } from 'countries-list';

const useDebounce = (value: any, delay: number) => {
  const [debounceValue, setDebounceValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebounceValue(value);
    }, delay);

    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debounceValue;
};

// Get the values of the countries and sort is alphabetically
const countryList = Object.values(countries)
  .map(country => ({
    ...country,
    lowerCaseName: country.name.toLowerCase(),
  }))
  .sort((a, b) => a.name > b.name);

const SearchableFlatList = () => {
  const [query, setQuery] = useState('');
  const debounceQuery = useDebounce(query, 300);
  const [filteredCountryList, setFilteredCountryList] = useState(countryList);

  useEffect(() => {
    const lowerCaseQuery = debounceQuery.toLowerCase();
    const newCountries = countryList
      .filter((country) => country.lowerCaseName.includes(lowerCaseQuery))
      .map((country) => ({
        ...country,
        rank: country.lowerCaseName.indexOf(lowerCaseQuery),
      }))
      .sort((a, b) => a.rank - b.rank);

    setFilteredCountryList(newCountries);
  }, [debounceQuery]);

  return (
    <View>
      <SearchBar
        placeholder="Search your countries..."
        onChangeText={setQuery}
        value={query}
      />
      <FlatList
        keyExtractor={(item, index) => `${index}`}
        data={filteredCountryList}
        renderItem={({item}) => <Text>{item.name}</Text>}
      />
    </View>
  );
};

export default SearchableFlatList;

Congratulations! We successfully build a searchable FlatList in React Native.

Hope you like the tutorial and able to build a performant search in React Native. I believe there are many ways to achieve this. If you found any better solution, please feel free to share it in the comment below!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top