How to create a real-time search using debounce in react.js

How to create a real-time search using debounce in react.js

This article shows how to create a real-time search functionality using debouncing in your react app where the search results update in real-time as the user types.

What are we building?

We’ll be creating a search page that contains a search box(where the user types in the query) and a list holding the search results(updates in real-time as the user types).

Looks something like this…

Let’s start with creating a react project-

> npx create-react-app my-app 
> cd my-app 
> npm start

This is how the project is structured-

Project structure for our react appProject structure for our react app

Since this article focuses on the front-end side, we’ll just use a public API(instead of creating our own) which returns a list of beers given the beer name as a query parameter.

API Endpointhttps://api.punkapi.com/v2/beers

Query Parameter — beer_name(String)

API Documentationhttps://punkapi.com/documentation/v2

First, let’s create a file utils.js that exports the function to fetch response from the API.

utils.js

export const BASE_URL = 'https://api.punkapi.com/v2';

/* This function takes query as a param and returns the array of beer objects.*/

export const fetchSearchResults = async query => {

if (query && query.length > 0) {

/* replaces all whitespaces in the query with + symbol in order to 
send it as a query param in the GET request */
const parsedQuery = query.replaceAll(' ', '+');

const url = `${BASE_URL}/beers?beer_name=${parsedQuery}`;

const res = await fetch(url);.search-container {
display: flex;
justify-content: center;
margin-bottom: 24px;
}

.search-container input {

height: 32px;
width: 300px;
padding: 0px 12px 0px 12px;
}

const resJson = res.json();

return resJson;

} else {

return [];

}

};

Now, we’ll need our list item, so let’s create a component ListItem.

ListItem.js

import React from 'react';
import './ListItem.css';

const ListItem = ({ title, caption, imageUrl }) => {

return (

<div class="list-item-container">
 <div className="left">
  <img src={imageUrl} className="thumbnail" />
 </div>
 <div className="center">
  <h4>{title}</h4>
  <p>{caption}</p>
 </div>
 <div className="right">
  <p>&#8250;</p>
 </div>
</div>

);
};

export default ListItem;

ListItem.css

.list-item-container {
 display: flex;
 padding: 8px;
 border-bottom: 1px solid #e6e6e6;
}

.center {
 flex: 4;
}

.left {
 flex: 1;
 display: flex;
 justify-content: center;
}

.right {
 flex: 0.2;
}

.right p {
 font-size: 32px;
 color: #000;
}

.thumbnail {
 width: 10px;
}

p,h4 {
 margin-block-start: 0px;
 margin-block-end: 8px;
 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

h4 {
 font-weight: 600;
}

p {
 color: #a1a1a1;
}

We’ll also need a search box where user can type in the query,

SearchInput.js

import React from 'react';

import './SearchInput.css';

const SearchInput = ({ value, onChangeText }) => {

React.useEffect(() => {

  /* Adds an event listener which fires whenever the value of our 
  input field changes and call the onChangeText function passed
  in as a prop to our component */
  let input = document.querySelector('input');
  input.addEventListener('input', onChangeText);

  /* Don't forget to return a cleanup function */  
  return input.removeEventListener('input', onChangeText);

 }, []);

return (
 <div class="search-container">
  <input
   type="text"
   value={value}
   onChange={onChangeText}
   placeholder="Search beer by name"
  />
 </div>
);

};

export default SearchInput;

SearchInput.css

.search-container {
 display: flex;
 justify-content: center;
 margin-bottom: 24px;
}

.search-container input {
 height: 32px;
 width: 300px;
 padding: 0px 12px 0px 12px;
}

What we have done so far -

  1. Define a function to call the API and return the result.

  2. Create a ListItem which will render a single search result.

  3. Create a search input where the user can type in queries.

Next step would be to render these components in App.js and calling our fetchSearchResults function to get the results.

App.js

import React from 'react';
import './style.css';
import { fetchSearchResults } from './utils';
import ListItem from './components/ListItem';
import SearchInput from './components/SearchInput';

export default function App() {

const [query, setQuery] = React.useState('');
const [results, setResults] = React.useState([]);

const fetchData = async () => {
  const res = await fetchSearchResults(query);
  setResults(res);
};

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

return (
<div>
 <SearchInput
  value={query}
  onChangeText={e => {
  setQuery(e.target.value);
  }}
 />
 {results.map((result, index) => (
  <div key={index}>
   <ListItem
    title={result.name}
    imageUrl={result.image_url}
    caption={result.tagline}
   />
  </div>
 ))}
</div>
);
}

Here’s the whole picture —

  1. Initially, we just have our search input box rendered on the screen because results state array is empty.

  2. As the user types in query, the event listener attached to input box fires and calls onChangeText function with the event details from which we get current value of the search box and we set it as the value of state variable query.

  3. Whenever the value of query changes, callback to useEffect executes fetchData() and the results from it populates the results array.

  4. As soon as results array has some items, react renders <listItem /> for each member of the array.

Right now, we have our real-time search working, but there’s a problem. Let’s add a console.warn('fetching') inside our fetchData() and run.

fetchData runs five times for a query of length 5.

The issue is it sends an API request to the backend every time the user enters a character or deletes a character. Too many requests, which is not desirable especially if you work with products at scale.

So, the solution — Debouncing.

Debouncing is the act of making a function fire only once in a certain time interval. For a more detailed explanation about Debounce in javascript, refer to the article **Debounce — How to Delay a Function in JavaScript (JS ES6 Example) by [Ondrej Polensky](freecodecamp.org/news/author/ondrej/).**

For example, we want the fetchData() function to wait for some time(in milliseconds) so that it fires only after the user types in some characters instead of after each edit on the input box.

Install lodash.debounce

$ npm i --save lodash.debounce

Let’s debounce our fetchData function by passing it as a callback to debounce(fn, milliseconds) provided by lodash.debounce .

inside App.js —

...

import debounce from 'lodash.debounce';

const fetchData = async (query, cb) => {
 const res = await fetchSearchResults(query);
 cb(res);
};

const debouncedFetchData = debounce((query, cb) => {
 fetchData(query, cb);
}, 500);

export default function App() {

const [query, setQuery] = React.useState('');
const [results, setResults] = React.useState([]);

React.useEffect(() => {
 debouncedFetchData(query, res => {
  setResults(res);
 });
}, [query]);

return (...);
}

We modified fetchData function to accept query string and a callback function as its parameters and it executes the callback function. Notice that the callback function also accepts the API results array as a param.

debouncedFetchData refers to the debounced version of fetchData returned by debounce() .

Also, fetchData has been moved outside the functional component and debouncedFetchData is also defined outside the component. This is done because local variables in a functional component are not preserved across re-renders.

Finally, we call debounceFetchData inside useEffect whenever value of query changes, passing in the query and seResults callback function as params.

And, it's done. Let’s see the difference —

Now, fetchData is called only once for a string of length 5 whereas earlier it was being called for each character.

Here’s the full source code — react-debounce-search - StackBlitz Starter project for React apps that exports to the create-react-app CLI.stackblitz.com

Thanks for reading and I hope you found the article interesting.

If you found the article helpful, please like and share it. Follow me on Hashnode to get regular updates when I publish a new article. You can also connect with me on LinkedIn and check out my Github.

Also, I would love to hear any feedback from you.