In this tutorial, we’ll learn how to optimize your search query using React debounce. The previous React Search filter tutorial example has one flaw. Every time a user types something, and one key event it calls a remote API, this will cause search performance issues and unnecessary calling of API. In some cases remote APIs are expensive.
In this tutorial, we‘ll learn how to optimize the performance of search using React debounce approaches. We’ll implement debounce using our own custom code and Lodash debounce methods. Let’s get started.
Issues with the search without React Debounce
Let’s first learn, why we need React debounce search approach, in our first search example let’s implement React search without debounce. Here is a screenshot of search without debounce approach.
In the above diagram, we can easily see that every time user types something we call search API for each character type. In most cases we don’t want to do that, there are better ways of handling search queries on remote API.
Step 1, we need to create React project and install material UI, and Axios library.
npx create-react-app debounce-search
cd debounce-search
npm install @mui/material @emotion/react @emotion/styled
npm install @mui/icons-material
npm i axios
We need to add the Material UI font and icon in the public/index.js for Material UI.
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
<link rel="stylesheet"
href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
Step 2: Create a Searchbar component
Let’s create a SearchBar component, as creating a component will allow us code reusability. In the components/SearchBar.jsx let’s add the following code for the search bar.
import React from 'react';
import InputAdornment from '@mui/material/InputAdornment';
import OutlinedInput from '@mui/material/OutlinedInput';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import SearchIcon from '@mui/icons-material/Search';
const SearchBar = (props) => {
return (
<FormControl sx={{ m: 2, width: '100ch' }}>
<InputLabel htmlFor='outlined-adornment-amount'>Search filter</InputLabel>
<OutlinedInput
id='outlined-adornment-amount'
onChange={(e) => props.onSearch(e.target.value.toLowerCase())}
value={props.value}
startAdornment={
<InputAdornment position='end'>
<SearchIcon />
</InputAdornment>
}
label='Search'
/>
</FormControl>
);
};
export default SearchBar;
Step 3: Create component Countries
Whatever search result we got from https://restcountries.com/ API using the search query, we display it search result in the Countries.jsx component. Before that we need to set the Axios API setting, Axios is a simple promise-based HTTP client for making HTTP requests.
In this example, we are retrieving a list of countries from free API restcountries.com this site allows us to make search queries on countries and more. Let’s create a folder called api inside the src and add restcountries.js: api/restcountries.js add configuration of Axios in it.
import axios from "axios";
export default axios.create({
baseURL: "https://restcountries.com/v3.1"
});
In the components/Countries.jsx file let add code for displaying search result countries from the search query. We have used the Material UI table component to display the record.
import React from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
const Countries = ({ countries }) => {
if (countries.length === 0) {
return;
}
return (
<Paper sx={{ maxWidth: 900, m: 2 }}>
<Table>
<TableHead>
<TableRow>
<TableCell>S.no</TableCell>
<TableCell>Name</TableCell>
<TableCell>Capital</TableCell>
<TableCell>Region</TableCell>
<TableCell>Population</TableCell>
</TableRow>
</TableHead>
<TableBody>
{countries.map((country, index) => {
return (
<TableRow key={index}>
<TableCell component="th" scope="row">
{index + 1}
</TableCell>
<TableCell>{country.name.common}</TableCell>
<TableCell>{country.capital}</TableCell>
<TableCell>{country.region}</TableCell>
<TableCell>{country.population}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Paper>
);
};
export default Countries;
The Countries component receives countries’ data as props from its parent component, which is the App component. When there is no search data, then just return and if countries’ props contain information, then we are displaying it using the Material table component.
Step 4: Perform remote API call on search queries
We have both SearchBar and Countries components to display the records, let’s now import both of these components inside our App.js file and also the restCountries API configuration for Axios.
import { useState } from "react";
import SearchBar from "./components/SearchBar";
import Countries from "./components/Countries";
import restcountries from "./api/restcountries";
function App() {
const [countries, setCountries] = useState([]);"
const onSearchChange = async (query) => {
if (query) {
const response = await restcountries.get(`/name/${query}`);
setCountries(response.data);
}
};
return (
<div style={{ margin: "10px" }}>
<SearchBar onSearch={onSearchChange} />
<Countries countries={countries} />
</div>
);
}
export default App;
Whenever the user types something on the SearchBar input, it will invoke the onSearchChange function in its parent component which is App. In the onSearchChange function make calls to remote API with search queries and whatever result we got from it is set to the countries variable and pass it as props in the Countries component.
React debounce search Implementation
I had given an example of React search without debounce so that we can understand what issues we face. To improve search optimization and avoid the unnecessary calling of remote API, we use debounce approach with search.
What is React debounce?
With debounce, we can control the timing of search and in debounce, we only perform a search after every x millisecond when user has stopped typing. Where x is custom or in our example we had given 1000 milliseconds. Thus we prevent unnecessary network calls from being made. In debounce, we are doing the following.
- Debounce the input, don’t search on every character user type and search only after x (x means custom time) millisecond when the user stops typing
- Discard any old searches, if the user has made a new search query
Here is a screenshot of React debounce input search
We can see from the diagram, the search is only performed when 1000 milliseconds or 1 second when the user stops typing the search.
Debounce React search example
All codes are the same, you don’t need to change any code from the previous example, except the App.js where we are implementing Debounce react search. We implement the debounce using useEffect hooks from React. This is an example of React debounce useEffect hook in a functional component. Let’s edit the App.js file
import { useEffect, useState } from "react";
import SearchBar from "./components/SearchBar";
import Countries from "./components/Countries";
import restcountries from "./api/restcountries";
function App() {
const [countries, setCountries] = useState([]);
const [searchQuery, setSearchQuery] = useState("");
const [debounceQuery, setDebounceQuery] = useState("");
useEffect(() => {
const timeOutId = setTimeout(() => {
if (searchQuery) {
setDebounceQuery(searchQuery);
}
}, 1000);
return () => {
clearTimeout(timeOutId);
};
}, [searchQuery]);
useEffect(() => {
const search = async () => {
const { data } = await restcountries.get(`/name/${debounceQuery}`);
setCountries(data);
};
if (debounceQuery) {
search();
}
}, [debounceQuery]);
const onInputChange = (query) => {
if (query) {
setSearchQuery(query);
}
};
return (
<div style={{ margin: "10px" }}>
<SearchBar onSearch={onInputChange} />
<Countries countries={countries} />
</div>
);
}
export default App;
I highlighted the code in which we modify React debounce input, and how we are achieving debounce search.
- We have extra variables, searchQuery, and debounceQuery. We have two useEffect hooks, we can add a dependency on these hooks.
- In the first useEffect hook we have a dependency on searchQuery, whenever the user types something, we call this hook, and only after 1000 milliseconds set search query to debounceQuery variable using setTimeOut function.
- We need to clean up when the component is unmounted, useEffect the return function we do all cleanup and here we clean the setTimeout function.
- Only in the second useEffect hook perform a search query to remote API using debounce as a search query.
React Lodash debounce with usecallback
Let’s implement React debounce input with Lodash debounce with useCallback and we can achieve the same result. The Lodash is a popular javascript library, that helps in working with arrays, strings, objects, numbers, etc. We don’t need to install the whole Lodash library, instead, we can install the Lodash debounce only.
npm i lodash.debounce --save
Along with Lodash debounce we use React hook the useCallback hooks and the useCallback hook returns a memoized callback function. This allows us to save resources needed for function, and isolate resource-intensive functions so that they will not automatically run on every render.
Here is React Lodash debounce example in action, you can see that only after 1000 millisecond when user stop searching will invoke an API call.
All codes are the same, except the App.js component, where we need to import debounce from lodash and useCallback from the react.
import { useCallback, useState } from "react";
import debounce from "lodash.debounce";
import SearchBar from "./components/SearchBar";
import Countries from "./components/Countries";
import restcountries from "./api/restcountries";
function App() {
const [countries, setCountries] = useState([]);
const handleDebounceFn = async (query) => {
const { data } = await restcountries.get(`/name/${query}`);
setCountries(data);
};
const debounceFn = useCallback(debounce(handleDebounceFn, 1000), []);
const onInputChange = (query) => {
if (query) {
debounceFn(query);
}
};
return (
<div style={{ margin: "10px" }}>
<SearchBar onSearch={onInputChange} />
<Countries countries={countries} />
</div>
);
}
export default App;
Whenever the user types something on the search input we invoke the onInputChange() function, which intern calls debounceFn(query), this function we had assigned with useCallback with the first argument with Lodash debounce function. The Lodash debounce function has two arguments, the first callback function for search handleDebounceFn, and 1000 is in a millisecond. Call the callback function handleDebounceFn is call only after 1000millisecond or 1 second.
Conclusion
We had learned when and why to use debounce in React and we implement debounce using two approaches. If the user is typing long phrases continue with debounce approach then if you feel that auto suggestion is not working, since the search function is called only after the user stopped typing after x millisecond. In such a case, we can use throttling, which is another search approach, which search only search after x milliseconds even user typing.
Related Post
- How to implement React table and its component?
- When and how to use React Context API?
- React Axios for HTTP method with REST API
- React router navigate outside component.
- How to implement React routing using react-router-dom?