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:
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;
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* .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
.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.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.debounce
function is then called, passing foo
as the function to be debounced and 800
as the debounce delay in milliseconds.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:
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....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.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.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.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.
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.