practical example of using closure

What is a debouncer?

A debouncer is a wrapper function that limits the number of executions of the function passed to it to a certain period of time.

Practical use

Suppose, when entering text into an input, we want to send a request to the server to get a drop-down list of options for the entered value.

By default, the request will leave when the next letter is entered, but in order not to load the server with requests and not repeat the rendering of the page with data of the same value, we want to wrap the request in a debouncer with some time limit.

Thus, the request will not be made when entering each next letter, but will be limited to a certain period of time.

How to implement a debouncer?

In order to correctly implement the debouncer, we need that each next call to the objective function “knows” about its previous call, and according to this data, the debouncer decides whether to execute the function or postpone.

For the debouncer, “this data” is the timeout identifier.

So what about closures?

And despite the fact that in order for a subsequent call to be able to access the timeout identifier, while limiting this identifier for external change, it is convenient to use a closure.

What is a closure?

A closure is a function that encapsulates and returns a function with its environment.

Quite simply, we can say that the environment – or lexical environment – is a block of code inside curly braces.

Variables declared in this environment (in particular, in a closure), firstly, will be inaccessible from outside this environment. It is a feature of the javascript language that only it and its child environments have access to environment variables, provided that access is requested after the declaration.

The second property is that the environment of the function is not removed from memory immediately after the execution of the function itself. Thus, variables declared in a closure can be “shared” between different calls to the function returned by the closure.

Let’s now see how it will be in practice.

First, let’s declare a debounce closure function that takes a callback function and its execution limit. It will contain a variable that stores the timeout ID of each subsequent call:

export const debounce = (callback, interval) => {
  let prevTimeoutId;

  return (...args) => {
    prevTimeoutId = setTimeout(() => {
      callback(args);
    }, limit);
  }
}

We pass the arguments of the returned function to the callback. Pay attention to the order in which the arguments are passed to the closure: …args are the arguments that the function will receive on the last (i.e. second) call – for example, it can be an event object if, say, a debouncer is passed as an event handler.

Further, the logic is as follows: on the next function call, we need to remove the previous call from memory – this can be done by the timeout ID using the clearTimeout function. Then we need to declare a new timeout, save its ID and return a new function:

const debounce = (callback, interval = 0) => {
  let prevTimeoutId;
 
  return (...args) => {
    clearTimeout(timeoutId);
    prevTimeoutId = setTimeout(() => callback(...args), interval);
  }
}

Now, if we now want to use our debouncer on the input, then it will look like this:

document.querySelector('input').addEventListener(
  'input',
  debounce(ev => console.log(ev.target.value), 1000)
);

And when entering characters into the input, only the values ​​\u200b\u200bentered in the interval of one second will be displayed on the console.

Let’s add reactivity!

In case we want to use a debouncer inside a react component, then it needs to be converted into a custom react hook so that the identifier of the previous timeout and the returned function do not disappear from memory. This is done using the useRef and useCallback hooks, respectively:

const useDebounce = (callback, interval = 0) => {
  const prevTimeoutIdRef = React.useRef();

  return React.useCallback(
    (...args) => {
      clearTimeout(prevTimeoutIdRef.current);
      prevTimeoutIdRef.current = setTimeout(() => {
        clearTimeout(prevTimeoutIdRef.current);
        callback(...args);
      }, interval);
    },
    [callback, interval]
  );
};

And if we plan to use it further in useEffect, then in order to avoid errors, we first need to initialize useDebounce with a callback and an interval into a variable, and only then call this variable in useEffect and pass the necessary arguments to it.

That’s all.

I hope you enjoyed this fascinating journey into the world of debouncers and closures.

I recommend the following article on closures:

https://learn.javascript.ru/closure

Thank you

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *