DEV Community

Debouncing with React Hooks

Gabe Ragland on January 17, 2019

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

}, [])

Collapse
 
awlevin profile image
Aaron Levin

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.

Collapse
 
awlevin profile image
Aaron Levin

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

Collapse
 
longfellowone profile image
Matt Wright

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

Collapse
 
cezarlz profile image
Cezar Luiz

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

Collapse
 
gbersac profile image
Guillaume Bersac

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

Collapse
 
gabe_ragland profile image
Gabe Ragland

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.

Collapse
 
jamison_dance profile image
Jamison Dance

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.

Thread Thread
 
raulmarindev profile image
Raúl Marín

This reply is great, thanks!

Collapse
 
ruchitgandhineu profile image
Ruchit Gandhi

Loved the explanation. Thanks!
Although I would use useRef hook to set the timer variable to reduce the heap memory usage.

const useDebounce = (value, delay = 500) => {
  const [debouncedValue, setDebouncedValue] = useState();
  const timer = useRef(null);
  useEffect(() => {
    timer.current = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    return () => clearTimeout(timer.current);
  }, [value, delay]);

  return debouncedValue;
};
Enter fullscreen mode Exit fullscreen mode

Edit how-to-use-debounce

Collapse
 
eransakal profile image
Eran Sakal

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?

Collapse
 
skyboyer profile image
Yevhen Kozlov

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

Collapse
 
skyboyer profile image
Yevhen Kozlov

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.

Collapse
 
marksgilligan profile image
Mark Gilligan

This was a great guide, had been struggling for a while with getting something like this to work.

Thanks!

Collapse
 
yaron profile image
Yaron Levi

Thanks! Helped me a lot

Collapse
 
alaindet profile image
Alain D'Ettorre

You've got a missing = here

const queryString `apikey=${apiKey}&titleStartsWith=${search}`;
Collapse
 
uladkasach profile image
Uladzimir Kasacheuski • Edited

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

Collapse
 
chrislovescode profile image
Chris

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

Collapse
 
storkontheroof profile image
Richard van den Winkel • Edited

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

Collapse
 
rohan2734 profile image
rohan2734 • Edited

this is not working, i am getting redundant calls, the function calls are not getting cancelled

Collapse
 
shakilaust profile image
shakil ahmed

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 ?

Collapse
 
gabe_ragland profile image
Gabe Ragland

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

Collapse
 
shakilaust profile image
shakil ahmed

Thank you so much, i will.

Collapse
 
alexdorosin profile image
Alex Dorosin

thank you, kind sir!

Collapse
 
oceandrama profile image
Ruslan Baigunussov

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?

Collapse
 
inboxdarpan profile image
Darpan

Very useful. I signed up only to thank you.

Collapse
 
ricardo5401 profile image
Ricardo Garcia Izasiga

Very nice code! :) very helpfull

Collapse
 
parhamzare profile image
ParhamZare

Thank you for sharing that, it's a very useful and simple way.

Collapse
 
mezzolite profile image
Mez Charney

This was incredibly helpful in figuring out how to debounce with my custom hook fetch. Thank you!