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 SectionList
.
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.
Without any further ado, let's get started!
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 (
`NULL`}
data={filteredCountryList}
renderItem={({ item }) => {item.name} }
/>
);
};
export default SearchableFlatList;
From the code snippet above, we are using open source countries-list
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 react-native-elements
. 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.
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.
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]);
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 (
`NULL`}
data={filteredCountryList}
renderItem={({item}) => {item.name} }
/>
);
};
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!
Copyright © 2024 Tek Min Ewe