Search code examples
javascriptreactjsclosuressettimeoutdebouncing

Debouncing in react is not working if I directly call the function containing logic for setTimeout


I was trying to implement the debounce functionality in React inside useEffect. Although for the below code, if I call debounceFunc its executing only once whereas if I directly call the debounceFunc3 its not working.

const handleClick = () => {
    setEvent((pre) => [...pre, 1]);
  };
  const delayFunc = () => {
    let timer;
    return function () {
      clearTimeout(timer);
      timer = setTimeout(() => console.log("Hello timer 1"), 2000);
    };
  };

  const delayFunc3 = useCallback(() => {
    let timer3;
    return function () {
      clearTimeout(timer3);
      timer3 = setTimeout(() => console.log("Hello timeout3"), 2000);
    };
  }, []);

  const debounce = useCallback(delayFunc(), []);

  useEffect(() => {
    if (event?.length > 0) {
      debounce();
      delayFunc3()();
    }
  }, [event]); 

In console Hello timer 1 is printed only once whereas Hello timeout3 is printed or each event triggered/changed.

Codebox link to play around:

https://codesandbox.io/s/inspiring-lamport-sbbup?file=/src/App.js:0-872

Couldn't understand what's wring with the way I am calling debounceFunc3


Solution

  • The problem with your code is that you're executing delayFunc3() multiple times:

    delayFunc3()();
    

    The issue with this is that every time you call delayFunc3()() you're creating a new local timer3 variable within your delayFunc3 function, and then executing the returned function that now references that new local timer3 variable.

    This means that if you call delayFunc3()();, first a new timer3 variable is created, and then the returned function is executed, queueing a new timeout.

    If you then call delayFunc3()();, again, a new timer3 variable is created (initialized to undefined), and then your returned function is executed. When your returned function is executed you're runnning clearTimeout(timer3);, which won't be clearing any timer as timer3 here is referring to the new timer3 variable that was initialized and is undefined, and not the previous timer3 that was created in the previous function delayFunc3()() call.

    As a result, calling delayFunc3()(); again while your first timeout is still running won't have any effect on the first timeout as the timer3 created by this invocation has no relation to the previous timer3 created, and so it won't be cleared.

    Unlike this, performing the following:

    const debounce = useCallback(delayFunc(), []);
    

    executes your delayFunc() and creates a new local variable timer within the scope of delayFunc(). The returned function from delayFunc() closes over the timer variable declared within the delayFunc() scope. This means that whenever debounce() is executed, you won't be creating a new timer variable like you were in your first example, but instead, you will be reusing the timer created from the initial delayFunc() call. Each call to debounce() will reuse the same timer variable, and so, if debounce() is called again, the clearTimeout(timer); will clear the previously queued timer.


    Also note that in both of your examples, you're using useCallback() on two different things. With debounce, you're calling useCallback() on the returned function from delayFunc(). This means that all future rerenders of your component will use the one function reference that was returned from the initial delayFunc() call when your component mounted. So debounce refers to the one unique function for all rerenders.

    Unlike this, delayFunc3 is using useCallback() not on the returned function, but rather the entire function (ie: the "parent" arrow function). This means that each rerender of your component will use the same "parent" function, but that parent function still creates a new timer3 variable when invoked and returns a new inner function each time it is called.