Implementing Debounce in React for Efficient Delayed Search Queries
Tags: javascript, react, typescript, webdev
Published: 2024-01-05 01:21:56


In this article, we will look at how to implement debounce in React for efficient delayed search queries. The problem we are trying to solve is that we want to make an API call to search for a user after the user has stopped typing for 1s (in real life, it would be more like 300ms). It will make the API calls more efficient and reduce the load on the server.

We will use the debounce function from my previous article [Let’s implement a Debounce function in Javascript](https://dev.to/alexefimenko/lets-implement-a-debounce-function-in-javascript-1ij1). But we will need to adjust it a bit to use it in a React component.

You can check how it works in the CodeSandbox below:

{% codesandbox jw28mj %}

The main problem to use the debounce function in React is that we need to store the timer ID between renders. If we just use a useState hook, the timer ID will be reset on every render. So for this, we will use the useRef hook as it is [recommended by the React team](https://react.dev/reference/react/useRef#referencing-a-value-with-a-ref).

Let's adjust the debounce function to use the useRef hook:

```typescript
const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);

function debounce(func, delay: number) {
  return function (...args: []) {
    if (debounceTimeout.current) {
     clearTimeout(debounceTimeout.current);
    } 
   debounceTimeout.current = setTimeout(() => {
      func(...args);
      debounceTimeout.current = null;
    }, delay);
  };
}
```


We are going to search through a list of countries. We will use the useState hook to store the search term and the search result. We will use the debounce function to create a debounced search function that will be called after the wait time has passed:

```typescript
const [searchTerm, setSearchTerm] = useState("");
const [searchResult, setSearchResult] = useState<string[]>([]);

const performSearch = (query: string) => {
  if (query.length === 0) {
    setSearchResult([]);
    return;
  }
  const result = countries
    .filter((country) => country.toLowerCase().includes(query.toLowerCase()))
    .sort();
  setSearchResult(result);
};
```

So the debounced search function will look like this:

```typescript
const debouncedSearch = debounce(performSearch, DELAY);
```

We will use the debounced search function in the input change handler:

```typescript
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { value } = event.target;
  setSearchTerm(value);
  debouncedSearch(value);
};
```

I prefer using Tailwind CSS for styling, so I added some styling to the component. 

{% details The full code of the component %} 
```typescript
import { useState, useRef } from 'react'
import { countries } from './countries'
import './App.css'

// set delay for debounce function in milliseconds
const DELAY = 1000

export default function App() {
  const [searchTerm, setSearchTerm] = useState('')
  const [searchResult, setSearchResult] = useState<string[]>([])
  const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)

  // Search function
  const performSearch = (query: string) => {
    if (query.length === 0) {
      setSearchResult([])
      return
    }
    const result = countries.filter((country) => country.toLowerCase().includes(query.toLowerCase())).sort()
    setSearchResult(result)
  }

  // Debounce function
  function debounce(func, delay: number) {
    return function (...args: []) {
      if (debounceTimeout.current) clearTimeout(debounceTimeout.current)
      debounceTimeout.current = setTimeout(() => {
        func(...args)
        debounceTimeout.current = null
      }, delay)
    }
  }

  // Debounced search function
  const debouncedSearch = debounce(performSearch, DELAY)

  // Event handler for input changes
  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target
    setSearchTerm(value)
    debouncedSearch(value)
  }
  const delayMessage = (
    <>
      <p>Delay in effect while typing... </p> <p>Search will run after 1 second of inactivity.</p>
    </>
  )
  const searchResultEmpty = searchResult?.length === 0
  // get first 20 results
  const searchResultList = searchResult.slice(0, 10).map((c) => <li key={c}>{c}</li>)
  return (
    <div className='flex flex-col gap-5'>
      <div className='h-12'>{debounceTimeout.current && delayMessage}</div>
      <div className='flex gap-5 items-center h-24'>
        <label htmlFor='search-input' className='text-xl'>
          Search:
        </label>
        <input
          className='border-2 border-gray-300 rounded-md px-3 py-2 text-lg'
          type='text'
          id='search-input'
          value={searchTerm}
          onChange={handleInputChange}
          placeholder='Type to search...'
        />
        <div className='w-12'>{debounceTimeout.current && <LoadingCircle />}</div>
      </div>
      <div className='h-64'>
        {{ searchResultEmpty } && <ul className='text-left list-disc list-inside text-lg'>{searchResultList}</ul>}
        {searchResultEmpty && searchTerm.length > 0 && !debounceTimeout.current && (
          <div className='text-lg animate-pulse'>No results found</div>
        )}
      </div>
    </div>
  )
}

function LoadingCircle() {
  return (
    <svg
      className='animate-spin h-12 w-12 text-gray-700'
      xmlns='http://www.w3.org/2000/svg'
      fill='none'
      viewBox='0 0 24 24'
    >
      <circle className='opacity-50' cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='4'></circle>
      <path fill='currentColor' d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z'></path>
    </svg>
  )
}

```
{% enddetails %}

You also can check my GitHub repository [debounce-react](https://github.com/aleksandr-efimenko/debounce-react-sandbox) for the full code.

I hope you found this article useful. If you have any questions or comments, please let me know in the comments below.

---

I'm always open to making new connections! Feel free to connect with me on [LinkedIn](https://www.linkedin.com/in/aleksandr-efimenko)