DEV Community

Cover image for Debounce function with a return value using promises
JorensM
JorensM

Posted on • Edited on

Debounce function with a return value using promises

In this article I will show you how to write a debounce function that allows you to return a value using promises.

What is a debounce function?

First of all, what is a debounce function? Simply put, a debounce function is a function that prevents another function from being called too often. When you call a debounce function, it sets a timer and runs the target function only after the timer has run out. But if the debounce function gets called again before the timer has run out, the timer gets reset, preventing the target function from being called repeatedly. So you can call the debounce function multiple times in a row, and the target function will only get called once, after the timer runs out.

This is especially useful in search inputs, where you only want to make a search request after the user has finished typing. Let me explain.

Without a debounce function, when you type into a search bar, for each character that you type, a HTTP request would be made. For example if you wanted to search for the term 'book', you would type 'b', and then a request would be made. Then you would type 'o', and another request would be made, and so on. Lots of pointless requests!

Whereas a debounce function allows you to postpone the search request until you have finished typing the search query. You would type 'b', then the debounce function would be called, starting the timer for the target function. Then you would type 'o', and the debounce function would get called once again, resetting the timer and preventing the target function from being called too early. Then you would type 'o', and the timer would get reset again. After typing the last character, the debounce function would get called one last time, and after the timer runs out, it would call your target function - in this case the search request. So only one HTTP request gets made, after the user has finished typing!

Below are two examples, without a debounce function and with a debounce function.

Without debounce function:

With debounce

With debounce function:

Without debounce

As you can see, example #1 updates the results each time you type a letter, whereas in example #2, the results only get updated after the user has finished typing.

In these examples, mock data is used and the results show up instantly. But if you used real data that gets fetched from a server, example #1 would be making lots of HTTP requests for every typed letter, clogging the queue. Whereas example #2 would only make one HTTP request, after typing in the full query.

Let's get coding!

Alright, now that we know what a debounce function is, let's see how it works.

The debounce function is actually surprisingly simple, and can be written in as little as 9 lines of code(and even less if you try):

function debounce( callback, delay = 300 ){
    let timer;
    return ( ...args ) => {
      clearTimeout( timer );
      timer = setTimeout( () => {
        callback( ...args );
      }, delay );
    }
}
Enter fullscreen mode Exit fullscreen mode

The function takes a callback and delay duration as arguments. First it stores an empty reference to the timer, to be used later. Next it returns a function (that we will then store in a variable and call). This function clears the timer, and then sets a timeout with our specified delay, after which our callback will be run. If the returned functions gets called again before the timer has run out, the timer gets reset and the callback function gets prevented from being called until the timer runs out.

This is how to use the debounce function:

function search( term ){
  console.log( `searched for ${term}` );
}

const debouncedSearch = debounce( search, 600 );
Enter fullscreen mode Exit fullscreen mode

Now we can call debouncedSearch() and it will debounce the search function. Let's see it in action.

First, let's try calling the search function several times in a row(without using debounce) and see what happens.

search( 'book' );
search( 'book' );
search( 'book' );
search( 'book' );
Enter fullscreen mode Exit fullscreen mode

Console1

As you can see, the search function got called 4 times.

Now let's see what happens if we do the same, but with debounce.

debouncedSearch( 'book' );
debouncedSearch( 'book' );
debouncedSearch( 'book' );
debouncedSearch( 'book' );
Enter fullscreen mode Exit fullscreen mode

Console2

As you can see, the search function only got called once, even though we called the debounce function 4 times! This is because we called the debounce function quickly one after another, so the callback got debounced (meaning that the timer got reset and the callback was prevented from being called)

If we waited until the timer ran out and then called the debounce function again, it would call the search function again(because the timer would run out)

setTimeout( () => {
  debouncedSearch( 'book' );
  setTimeout( () => {
    debouncedSearch( 'book' );
    debouncedSearch( 'book' );
      setTimeout( () => {
      debouncedSearch( 'book' );
      debouncedSearch( 'book' );
      debouncedSearch( 'book' );
    }, 1000 );
  }, 1000 );
}, 1000 );
Enter fullscreen mode Exit fullscreen mode

Console3

Let's try implementing the search input from the earlier example using the debounce function

HTML:

<input type='text' id='search-input' placeholder='Search'/>

<h4>Results:</h4>
<ul id='results'>

<ul/>
Enter fullscreen mode Exit fullscreen mode

JS:

//Mock data
const data = [
  'Book',
  'Book Number One',
  'Book Number Two',
  'Book Number Three',
  'Book Numero Uno',
  'Book Numero Dos',
  'Book Numero Tres',
  'Book #1',
  'Book #2',
  'Book #3'
]

//Results list element
const results_list_element = document.getElementById( 'results' );
const search_input_element = document.getElementById( 'search-input' );

//This function renders specified results into the results element
function renderResults( results ){
  //Clear the results element
  results_list_element.innerHTML = '';
    //Loop through results
  for ( const result of results ) {
      //For each result, append an element to the results list
    results_list_element.insertAdjacentHTML( 'beforeend', `<li>${result}</li>`);
  }
}

//This function filters data by term and then renders the results
function search( term ){
  const results = data.filter( item => item.toLowerCase().includes( term.toLowerCase() ) );

  renderResults( results );

}

//Debounce function
function debounce( callback, delay ) {
  let timer;

  return( ...args ) => {
       clearTimeout(timer);
       timer = setTimeout( () => {
         callback( ...args );
       }, delay );
  }
}

//Debounced search()
debouncedSearch = debounce(search, 600);

//Show all results before user has typed anything
renderResults( data );

//Add event listener for searching
search_input_element.addEventListener( 'input', e => {
  //Call debounced search with the input value
  debouncedSearch(e.target.value);
})
Enter fullscreen mode Exit fullscreen mode

Below is a working CodePen:

So far we have learned how to implement and use the traditional debounce function.

A caveat of the traditional deboucne function that we used here is that it does not allow the callback to return a value. This forces us to have to write all the logic right in the callback function, since we can't extract any data from it. But luckily there is a solution.

Promises to the resuce!

With the help of promises, we can rewrite our deboucne function in such a way that it returns a promise that we can then then() to access the callback's return value.

This is what the updated debounce function looks like:

//Debounce function with return value using promises
function debounce( callback, delay ) {
  let timer;

  return( ...args ) => {
    return new Promise( ( resolve, reject ) => {
      clearTimeout(timer);
      timer = setTimeout( () => {
          try {
            let output = callback(...args);
            resolve( output );
          } catch ( err ) {
            reject( err );
          }
      }, delay );
    })

  }
}
Enter fullscreen mode Exit fullscreen mode

The difference from the previous implementation is that we're returning a promise instead of a regular function, and then resolve/reject it with our callback's return value once the timer runs out. This allows us to add a return value to our callback, which we can then access with the 'then()' function, like so:

//Filters data by term and then returns the results
function search( term ){

  const results = data.filter( item => item.toLowerCase().includes( term.toLowerCase() ) );

  return results;

}

search_input_element.addEventListener( 'input', e => {
  //Call debounced search with the input value
  debouncedSearch(e.target.value)
  .then( results => renderResults( results ) )
  .catch(err => console.log(err.message));
})
Enter fullscreen mode Exit fullscreen mode

Below is a CodePen with the updated debounce function. Visually speaking it behaves exactly the same way as the previous CodePen, but internally we are extracting the search results from the callback, instead of running all code right in the callback.

Conclusion

In this article you learned about the debounce function, its use case and how to implement one.

If you have any questions, comments or suggestions, feel free to leave them in the comments!

Thank you very much for reading and I hope you learned something new!

Top comments (1)

Collapse
 
jdnichollsc profile image
J.D Nicholls

Oh uh, The problem is your code is going to call a Promise that's going to live forever because you're using a timer to clear that, so basically the Promise is not going to resolve or reject that call, that's not good for the Event-Loop because you're going to have async functions that are going to run forever in memory :/