loading...

React Debounce Debouncing with React Hooks

gabe_ragland profile image Gabe Ragland Updated on ・3 min read

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 [];
    });
}

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;
}

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.

Here's the Marvel Comic Search demo on CodeSandbox.

If you enjoyed this post also be sure to check out my React hooks blog and my React app builder.

Posted on Jan 21 by:

gabe_ragland profile

Gabe Ragland

@gabe_ragland

Building divjoy.com. Made usehooks.com. YC alum. ☕️ ⚛️ 🐶 🍔 ☀️

Discussion

markdown guide
 

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);
  };
};

Now you can use this debouce in other hooks.

 

how do you make the call debounce function ?

 

Great article!

I've been using this pattern with hooks lately:

const [value, setValue] = useState()
const debouncedValue = useDebounce(value, 800)

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

 

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.

 

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]
    )
}

 

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

 
 

How could you modify this to make it a "leading" debounce?

 

Hi,
Thanks for the article. I'm curious about the chosen approch of providing a value to the custom hook that gets updates over time.

export default function useDebounce(value, delay) {

Here we listen to value changes in the custom hook with the inner useEffect and react to changes. Most of the hooks I'm familiar with treat the arguments they get as sort of initialization values that doesn't change overtime or even if changed are not monitored. For example useState, useReducer, useSelector, useMemo, useCallback, useRef are all hooks that doesn't react to changes of the initial values provided to the hook.

I know that your sample works and is valid implementation but I'm sure if it will not confuse developers who read the code. you could easily expose a second argument to update the value which will look very similar to useState

wdyt?

 

useCallback, useSelector, useMemo, useReducer are actually operating on most recent arguments each run.
It's only useState and useRef who purposely ignore any changes on sequential calls

 

Why using useEffect for the api call? I don't see any issue with doing async api calls inside a component rendering function.

 

Any side effects should be wrapped in useEffect or within an event handler like onClick. Not sure what could go wrong by inlining it like that, but know it’s heavily discouraged by the React team.

 

You wrap side effects in useEffect so they don't run on every render. You technically could make a request inside render without wrapping it in useEffect, but it'll happen on every single render, which is usually not what you want.

You typically only want side effects to run when things they care about change, like when some search text changes. That's exactly what useEffect does for you - it helps make sure the side effects only run when they need to.

 

Awesome! I had a similar project and this looked easier than I thought!

 

Hmm this doesn't seem to work if you add text after the initial query (i.e. 'wolverine' as the first full term to debounce, then appending it to become 'wolverine x-men'.. the 'x-men' part doesnt show up until the debouncing times out). I feel like this code is so short and simple I should be able to figure it out, but I can't seem to see why the UI lags so hard on the second set of characters.

 

Aha, I take it back! Turns out I needed to pull my search input into its own component. Problem solved! (I think)

 

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.

 
 

Amazing, did not think to have debouncedValue instead of debounced version of callback.
Probably we better cancel processing in main useEffect to avoid race conditions.

 

This is really elegant! Have you thought about publishing this as a node_module?

 

Great read!
Just as a side note: you don't have to import react to your use-debounce.js.
" import { useState, useEffect } from 'react';"is just fine

 

Great work, thanks a lot. I want to write a similar post in my native language Bangla, is this ok if i use your post as reference and also your demo project to show how this work ?

 

Yeah sure! Please just link to my post as well somewhere.

 
 

Try to hold down any key and you will see, that searching appears 2 times - after 500 ms after the first character and after 500 ms after the last. How to avoid this glitch?

 

Very useful. I signed up only to thank you.

 

Excellent!
Needed this and was struggling a bit myself to get it to work flawlessly...
Thanks!