Debounce

Overview

Debouncing is a technique used to limit the frequency of function calls, particularly in scenarios where the function is triggered rapidly or repeatedly, such as in response to user input.


There are a lot of things that happen super quickly in the browser; for example, an event function is triggered every time the user:

  • Resizes the window
  • Inputs text
  • Moves mouse around

We often want to wait until the user has stopped the action and then perform an action based on the value of the action.


Take for example a user typing into a search field:

Let's say we were to send something to the backend in the onChange method; it would send H, He, Hel, Hell, Hello to the server. Instead, we want to wait until the user has finished typing and then send the Hello to the server.


For the solution, we could come up with a hook that handles the debouncing, and also takes care of not re-creating the function on state changes.

That's alot to digest... (very rarely things in React are simple).


Let's break it down;


  1. We initialize a ref using the useRef hook. The ref will be used to store the latest version of the callback function and will persist throughout re-renders. It's essentially memoization* .
  2. The useEffect hook is used to update the ref whenever the callbackFn changes. The effect runs whenever callbackFn changes, and it updates the ref.current value to hold the latest version of the callbackFn.
  3. The useMemo hook is used to create a debounced version of the callback function. It takes two parameters: the debounced function itself and an empty dependency array [] since we only want to create the debounced function once on mount.
  4. Within the debounced function, there is an inner function named foo that retrieves the latest version of the callback function from the ref using ref.current. The ?. operator is used for optional chaining to safely call the callback function if it exists.
  5. The debounce function is then called, passing foo as the function to be debounced and 800 as the debounce delay in milliseconds.
  6. Finally, we return the debouncedCallbackFn from this hook.

By using the useDebounce hook, you can obtain a debounced version of a callback function that will only execute after a certain delay (in this case, 800 milliseconds) since the last time it was called and it always has access to the latest state value (in this case, the value from the text input). This can be useful in scenarios where you want to optimize performance by reducing unnecessary function calls/re-creations triggered by re-renders.


Let's return to the actual debounce function:

The concept may not be the easiest to understand and not understanding it is perfectly OK. However, I've tried to break it down so it makes more sense:


  1. It declares a variable timeoutId and initializes it to null. This variable will store the identifier of the timeout that will be set to delay the execution of the callback.
  2. It returns an anonymous function* that acts as the debounced version of the callback function.
  3. When the debounced function is called (with any number of arguments represented by ...args), it first clears any existing timeout by calling window.clearTimeout(timeoutId). This ensures that the previous timeout is canceled and the debounced function will only be executed after the specified delay since the last time it was called. This way there are no excess timeouts in memory.
  4. After clearing the timeout, a new timeout is set using window.setTimeout. The setTimeout function takes two parameters: a callback function and the delay in milliseconds wait. The callback function is an anonymous function that will invoke the original callback function callback.apply(null, args) with the same arguments passed to the debounced function.
  5. The apply method is used to call the callback function with a null context* (the this value) and the args array as arguments. This ensures that the original callback function is executed with the correct context and arguments.
lightbulb

Why not use Lodash?

While Lodash and Underscore.js have been very popular in the past providing developers an easy way to use general utility methods without extending JS's built-in objects, JS has gotten so good that I prefer to not have the technical overhead of taking in a library and therefore increasing the bundle size (you can't only install the debounce method of the library), and also keeping it up-to-date. The less relying on third-parties, the better.

Usage

We would then use it as follows:

Go on and test the debouncing in action with the interactive demo!

Before you head out

Keep a few things in mind when using this function in your projects;

  • Delay: Consider the appropriate debounce delay for your use case. The delay determines how long the debounced function waits before executing after the last call. Choosing the right delay depends on the specific requirements of your application and the desired user experience. A shorter delay may provide more immediate feedback, but it can also lead to more frequent function calls. A longer delay may reduce unnecessary function calls but can introduce perceptible delays in responsiveness.

  • Function Side Effects: Be aware of any side effects caused by the debounced function. If the original callback function performs any asynchronous operations, network requests, or updates application state, consider how the debounced behavior may impact the desired functionality. Ensure that the debounced function still accomplishes the intended behavior and doesn't introduce any unexpected issues.

  • Performance: Consider the performance implications of using debouncing. While debouncing can help optimize performance by reducing unnecessary function calls, it also introduces a delay before the callback is executed. Evaluate whether the delay introduced by debouncing is acceptable for your use case and if it aligns with the desired user experience.

  • Dependency Arrays: Take into account the dependencies used in the useEffect and useMemo hooks. In the provided code, the useEffect hook depends on callbackFn, meaning that the ref.current value will be updated whenever callbackFn changes. If you have other dependencies related to the debounced function or the surrounding component, make sure to include them in the dependency arrays to avoid stale values or unintended behavior.