DEV Community

Cover image for Implementing Debounce in React for Efficient Delayed Search Queries
Alex
Alex

Posted on • Updated on • Originally published at blog.alexefimenko.com

Implementing Debounce in React for Efficient Delayed Search Queries

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. 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:

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.

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

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);
  };
}
Enter fullscreen mode Exit fullscreen mode

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:

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);
};
Enter fullscreen mode Exit fullscreen mode

So the debounced search function will look like this:

const debouncedSearch = debounce(performSearch, DELAY);
Enter fullscreen mode Exit fullscreen mode

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

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  const { value } = event.target;
  setSearchTerm(value);
  debouncedSearch(value);
};
Enter fullscreen mode Exit fullscreen mode

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

The full code of the component
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>
  )
}

Enter fullscreen mode Exit fullscreen mode

You also can check my GitHub repository debounce-react 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

Top comments (0)