Implementing Debounce in Vanilla Javascript
Possibly one of the most used functions in the world of Javascript is (besides throttle) debounce. So much so that many times it is the only function I end up importing from lodash, but, as small as the lodash
library may be, isn't it too much to import a new library just for one function?
That's why many times it can be more beneficial to implement our own versions of these types of utilities, and best of all, debounce is very simple to use.
What is debounce
?
When we talk about debounce, we refer to a technique used in programming (in Javascript mainly) in which we prevent an event from being executed until a certain time has passed. This helps us prevent an event from being executed before we have the tools it needs.
The Problem
Before we start, let's imagine that we have a search form in our application, this will send a request to our API to return results related to the keywords the user chooses. To help the user save precious milliseconds and a click, we will not add a search button, the form will do it automatically:
1<form>2 <label htmlFor='search1' className='block text-sm font-medium text-gray-700'>3 Enter a keyword4 </label>5 <input6 type='text'7 id='search1'8 name='search1'9 className='mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm'10 />11</form>12
This will result in:
Now, let's say that before connecting our form to our API, we want to make sure it works, so we want to print the value of this field in the console (you can open the browser console and try the field yourself 😉️)
Simply add a console.log
to the change function:
1<form>2 <label htmlFor='search1' className='block text-sm font-medium text-gray-700'>3 Enter a keyword4 </label>5 <input6 type='text'7 id='search1'8 name='search1'9 className='mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm'10 onChange={(e) => {11 console.log(e.target.value);12 }}13 />14</form>15

Can you see the problem? Every time we press a key our change function is executed, imagine that we had connected our form to the API, each of those logs would be a request to the API, and just from one user!
Now imagine that we had 10,000 users a day…
The Solution
This is where debounce comes in, what we are going to do is wait for the user to finish writing before executing our change function, we don't need to wait long, just a few milliseconds. This way we make sure
The logic of debounce is not very complicated, it is based on some very simple guidelines, which are:
- An event is executed (such as pressing a key)
- A timer is started
- If the same event is executed again:
- The timer is cleared
- A new timer is started
- If the timer manages to reach
0
, the function is finally executed
Now that we have the logic we need, we can move on to the fun part.
The Implementation
Since this is a tutorial to show the operation of debounce, I will not resort to my usual method of using it (since my usual method is simply to use lodash), instead, we will take the logic we wrote in the previous paragraphs and translate it to Javascript.
Let's start with the first step:
An event is executed (such as pressing a key)
Fortunately, in our example we are using the onChange
event, so this already takes care of that step.
This blog, and its examples, are written in React.
onChange
is an event that is automatically added to input
elements when they are created in React, this is not available in conventional Javascript, but in that case we can use the equivalent:
1const input = document.querySelector('#search1');2input.addEventListener('change', (event) => {3 console.log(event.target.value); // Here goes our logic4});5
Before we get to the change event, it's a good idea to save our function so we can use it in multiple places, so we'll create the function:
1const debounce = () => {};2
Pretty simple, now let's see.
A timer is started
To create the timer, first we need to save it in a reference (so we can clear it if necessary):
1const debounce = (callback, delay) => {2 let timer;34 return (...args) => {5 // Create a new timer.6 timer = setTimeout(() => {7 // Execute the function with the arguments.8 callback(...args);9 }, delay);10 };11};12
return (...args)
?
If you find it strange that we create a function that returns another function, the reason is that we need
a function that receives the arguments of the event, but that is executed after our timer.
When we see: addEventListener('change', debounced)
, what happens is that debounced
is executed first,
and then change
sends the arguments to the function that debounced
returns.
If the same event is executed again:
- The timer is cleared
- A new timer is started
So far we already have our timer, and we have specified that when it finishes it will execute our function,
(this takes care of point 4), but we are missing point 3, clear the timer if the event is repeated.
Since we already have a reference to our timer, we can clear it with clearTimeout
:
1const debounce = (callback, delay) => {2 // Save reference to the timer.3 let timer;45 return (...args) => {6 // Clear the timer if it exists.7 clearTimeout(timer);89 // Create a new timer.10 timer = setTimeout(() => {11 // Execute the function with the arguments.12 callback(...args);13 }, delay);14 };15};16
Our function is complete! To use it with our example, instead of starting our original function, what we do is wrap it in our debounce
function, like this:
1const debounced = debounce((e) => {2 console.log(e.target.value);3}, 500);4
Translating this, it would look like this:
1(e) => {2 console.log(e.target.value);3};4
This would be our callback
, and 500
would be the waiting time in milliseconds, our delay
.
Now, instead of using our original function, we pass our debounced
function to the onChange
event:
1<input2 type='text'3 id='search1'4 name='search1'5 className='mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm'6 onChange={debounced}7/>8
Or without using react:
1input.addEventListener('change', debounced)`}2/>3
Now, if we try our search form again, we will have something similar to this:
You can try the previous example in the browser console, 😉

After this, we can see a great improvement in our calls, it no longer records each and every letter we put in the field, but waits for the user to finish writing before making the call, resulting in many fewer and more complete calls to our API.
Conclusion
As we can see, this small function can be of great help, especially when we pay attention to the user experience, in many cases, it can benefit us to pause for a moment and wait for the user to give us some feedback before continuing with our calculations.
If you have any questions or comments, don't hesitate to contact me, I would love to hear your opinion!