DEV Community

Gabe Ragland
Gabe Ragland

Posted on • Edited on

React Debounce Debouncing with React Hooks

Today I'm going to show you how to build a useDebounce React Hook that makes it super easy to debounce API calls to ensure that they don't execute too frequently. I've also put together a demo that uses our hook. It searches the Marvel Comic API and uses useDebounce to prevent API calls from being fired on every keystroke.

demo screenshot

Pretty nifty huh? Okay, now on to the code!

First let's figure out how we want our hook to be used and we can let that guide or actual implementation of the hook logic. Rather than debounce the calling of our API request we're going to design this hook to debounce any value within our component's render function. We're then going to combine this with useEffect to fire off a new API request whenever that input value changes. This code example assumes some familiarity with the useState and useEffect hooks, which you can learn about in the React Hook docs.



import React, { useState, useEffect } from 'react';
import useDebounce from './use-debounce';

// Usage
function App() {
  // State and setter for search term
  const [searchTerm, setSearchTerm] = useState('');
  // State and setter for search results
  const [results, setResults] = useState([]);
  // State for search status (whether there is a pending API request)
  const [isSearching, setIsSearching] = useState(false);

  // Now we call our hook, passing in the current searchTerm value.
  // The hook will only return the latest value (what we passed in) ...
  // ... if it's been more than 500ms since it was last called.
  // Otherwise, it will return the previous value of searchTerm.
  // The goal is to only have the API call fire when user stops typing ...
  // ... so that we aren't hitting our API rapidly.
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  // Here's where the API call happens
  // We use useEffect since this is an asynchronous action
  useEffect(
    () => {
      // Make sure we have a value (user has entered something in input)
      if (debouncedSearchTerm) {
        // Set isSearching state
        setIsSearching(true);
        // Fire off our API call
        searchCharacters(debouncedSearchTerm).then(results => {
          // Set back to false since request finished
          setIsSearching(false);
          // Set results state
          setResults(results);
        });
      } else {
        setResults([]);
      }
    },
    // This is the useEffect input array
    // Our useEffect function will only execute if this value changes ...
    // ... and thanks to our hook it will only change if the original ...
    // value (searchTerm) hasn't changed for more than 500ms.
    [debouncedSearchTerm]
  );

  // Pretty standard UI with search input and results
  return (
    <div>
      <input
        placeholder="Search Marvel Comics"
        onChange={e => setSearchTerm(e.target.value)}
      />

      {isSearching && <div>Searching ...</div>}

      {results.map(result => (
        <div key={result.id}>
          <h4>{result.title}</h4>
          <img
            src={`${result.thumbnail.path}/portrait_incredible.${
              result.thumbnail.extension
            }`}
          />
        </div>
      ))}
    </div>
  );
}

// API search function
function searchCharacters(search) {
  const apiKey = 'f9dfb1e8d466d36c27850bedd2047687';
  const queryString `apikey=${apiKey}&titleStartsWith=${search}`;
  return fetch(
    `https://gateway.marvel.com/v1/public/comics?${queryString}`,
    {
      method: 'GET'
    }
  )
    .then(r => r.json())
    .then(r => r.data.results)
    .catch(error => {
      console.error(error);
      return [];
    });
}



Enter fullscreen mode Exit fullscreen mode

Okay, so that looks pretty good! Now let's build the actual hook so that our app works.



import React, { useState, useEffect } from 'react';

// Our hook
export default function useDebounce(value, delay) {
  // State and setters for debounced value
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(
    () => {
      // Set debouncedValue to value (passed in) after the specified delay
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      // Return a cleanup function that will be called every time ...
      // ... useEffect is re-called. useEffect will only be re-called ...
      // ... if value changes (see the inputs array below). 
      // This is how we prevent debouncedValue from changing if value is ...
      // ... changed within the delay period. Timeout gets cleared and restarted.
      // To put it in context, if the user is typing within our app's ...
      // ... search box, we don't want the debouncedValue to update until ...
      // ... they've stopped typing for more than 500ms.
      return () => {
        clearTimeout(handler);
      };
    },
    // Only re-call effect if value changes
    // You could also add the "delay" var to inputs array if you ...
    // ... need to be able to change that dynamically.
    [value] 
  );

  return debouncedValue;
}



Enter fullscreen mode Exit fullscreen mode

And there you have it! We now have a debounce hook that we can use to debounce any value right in the body of our component. Debounced values can then be included in useEffect's input array, instead of the non-debounced values, to limit the frequency of that effect being called.

Also check out my React codebase generator. It will give you a nice UI, auth, database, payments and more. Thousands of React devs use it to build and launch apps quickly.

Top comments (47)

Collapse
 
jivkojelev91 profile image
JivkoJelev91 • Edited

Why not just:

export const debounce = (func, wait) => {
  let timeout;
  return function(...args) {
    const context = this;
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      timeout = null;
      func.apply(context, args);
    }, wait);
  };
};
Enter fullscreen mode Exit fullscreen mode

Now you can use this debouce in other hooks.

Collapse
 
johanse7 profile image
johanse7

how do you make the call debounce function ?

Collapse
 
dextermb profile image
Dexter Marks-Barber
const onChange = debounce(event => setQuery(event?.target?.value))
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
rohan2734 profile image
rohan2734 • Edited

nope not working , geting the state value as undefined

Thread Thread
 
dextermb profile image
Dexter Marks-Barber

Are you able to show your code for the event handler and how you trigger the call?

Thread Thread
 
rohan2734 profile image
rohan2734

i have triggered the call in the same way, but it is not working at all ,and state is undefined

Collapse
 
daxsoft profile image
Michael Willian Santos

My aproach is this way:

import { useMemo, useState } from 'react'

/**
 * Debounce a function by time
 * @param {Function} func
 * @param {Number} delay
 */

export default function useDebounce(func, delay) {
    const [id, setId] = useState(null)

    return useMemo(
        (...args) => {
            if (id) {
                clearTimeout(id)
            } else {
                setId(
                    setTimeout(() => {
                        setId(null)
                        func(...args)
                    }, delay)
                )
            }
        },
        [func]
    )
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
gusyavo profile image
Gustavo

This looks great! Could you give us an example on how to implement it?

Collapse
 
georgewl profile image
George WL

whilst I like the idea of this, I think the UI and the API call should be far more decoupled, so instead I've went for:

// in the useDebounce file
import { useEffect } from 'react';
/**
 * @description for use in functions with side-effects but no return value
 * @export useDebouncedFunction
 */
export default function useDebouncedFunction(handler, watchedValue, delay) {
  useEffect(() => {
    const timeoutHandler = setTimeout(() => {
      handler();
    }, delay);
    return () => {
      clearTimeout(timeoutHandler);
    };
  }, [watchedValue, delay]);
}

// in the search file
  const [searchQuery, setSearchQuery] = React.useState('');

  const fetchX = async () => {
    await getX()
      .then((updatedStock: IStockResponse) => {
        setX(updated.data?? []);
      })
      .catch(err => {
        console.error(err);
      });
  };
  useDebouncedFunction(fetchStockVehicles, searchQuery, 1000);
  const handleSearch = (value: string) => {
    setSearchQuery(value ?? '');
  };

Enter fullscreen mode Exit fullscreen mode
Collapse
 
gnbaron profile image
Guilherme Baron

Great article!

I've been using this pattern with hooks lately:

const [value, setValue] = useState()
const debouncedValue = useDebounce(value, 800)
Enter fullscreen mode Exit fullscreen mode

It's clean and works well for most of the cases.

Here's a version useDebounce implemented using lodash: github.com/gnbaron/use-lodash-debo...

Collapse
 
haraldson profile image
Hein Haraldson Berg • Edited

Here’s my take on a lodash.debounce hook. I don’t see why the hook shouldn’t be more convenient to use, so I basically made a useState wrapper which updates the value immediately (a requirement for controlled inputs), and updates a signal, which is meant to be used in a useEffect’s dependency array, only whenever specified as per lodash.debounce’s docs.

Collapse
 
whenmoon profile image
Tom Belton • Edited

Great - thanks! The example would be easier to read if you included all the imports and exports. Great job.

Collapse
 
gabe_ragland profile image
Gabe Ragland

Good call! Just updated the post.

Collapse
 
whenmoon profile image
Tom Belton

Awesome! :)

Collapse
 
rohan2734 profile image
rohan2734

why dont we do this in the useEffect?

import React, { useState, useEffect } from "react";

const useDebounce = (data, delay) => {
  const [debouncedData, setDebouncedData] = useState(data);
 var timerID;
  useEffect(() => {

    if(timerID){
       clearTimeout(timerID)
    } 
    console.log("debounce executed");
     timerID = setTimeout(() => {
      setDebouncedData(data);
    }, delay);
   // return () => {
      //clearTimeout(timerID);
    //};
  }, [data.blogTitle, data.blogDescription]);

  return debouncedData;
};

export default useDebounce;


Enter fullscreen mode Exit fullscreen mode

i tried this but the cleartimeout was not working, the timeout was not being cleared, i didnt understand the reason.

can anyone explain?

Collapse
 
nexuszgt profile image
Giancarlos Isasi

Thanks for share!

Collapse
 
clearwaterstream profile image
Igor Krupin • Edited

Very helpful, thank you! Years ago I needed something like this and ended up using throttle feature of redux sagas. Not bad, but I try to avoid redux (gets a bit unreadable over time).

I'm writing a new app and needed an "auto-save" feature where a form is auto-saved every x seconds, provided any of the inputs were changed. I started doing my own thing (setTimeout, setInterval) and luckily stumbled on this post. Saved me time. Elegant and it works.

Throttling API calls from UI is a very important technique to learn, I wish Facebook would throw some sort of "autocomplete" or "lookup text field" example where they use your code.

Collapse
 
rezuanriyad profile image
Rezuan Ahmed
export default function useDebounce(func, delay) {
  const [timer, setTimer] = useState(null)
  return (...args) => {
    clearTimeout(timer)
    let _temp = setTimeout(() => {
      func(...args)
    }, delay)
    setTimer(_temp)
  }
}
Enter fullscreen mode Exit fullscreen mode
import useDebounce from './customHooks/useDebounce'

export default function App() {
  const [val, setVal] = useState("")
  const handleSearch = useDebounce((e) => {
    // api call
  }, 100)

  return (
    <div>
      <input
        onChange={(e) => setVal(e.target.val)}
        onKeyDown={handleSearch} />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
strap8 profile image
Nathan Foster • Edited

This makes the most since:

import { useCallback, useRef } from 'react'
export default function useDebounce(func, delay = 400) {
let debounce = useRef(null)
return useCallback(
(...args) => {
const context = this
clearTimeout(debounce.current)
debounce.current = setTimeout(() => {
func.apply(context, args)
}, delay)
},
[func],
)
}

Usage:

const handleWindowResize = useDebounce(SetWindow)

useEffect(() => {
window.addEventListener('resize', handleResize)

handleResize()

return () => {
  window.removeEventListener('resize', handleResize)
}
Enter fullscreen mode Exit fullscreen mode

}, [])