I was implementing the sign up function for TripTime, and added a onChange handler for the email input to send an API request to check if the email address has already been taken.

immediate checking

And the API server is dying.

There needs to be a delay after the user stops typing before the event handler gets invoked, and my teammate suggested that debounce is the solution.

The debounce function delays the processing of the keyup event until the user has stopped typing for a predetermined amount of time.

Read the debounce code

First let’s have a look at what a debounce function should look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Credit David Walsh (https://davidwalsh.name/javascript-debounce-function)

// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
function debounce(func, wait, immediate) {
var timeout;

return function executedFunction() {

// First we save the context of this and the contents of the arguments passed to executedFunction.
var context = this;
var args = arguments; // In JavaScript, you can call a function with an arbitrary number of parameters even if they aren’t in the function definition, and arguments will still capture them.

// The callback function, later, is executed after the end of the debounce timer.
var later = function() {
//The timeout is set to null which means the debounce has ended
timeout = null;
// Then it checks to see if we want to call the debounced func on the tail end. If we do, then it executes func.apply(context, args).
if (!immediate) func.apply(context, args);
};

// A callNow === true event will cause func to be executed immediately and then prevent any subsequent calls unless the the debounce timer has expired.
var callNow = immediate && !timeout;

// Next, we clearTimeout which had prevented the callback from being executed and thus restarts the debounce
clearTimeout(timeout);

// we (re-)declare timeout which starts the debounce waiting period
timeout = setTimeout(later, wait);

// If the full wait time elapses before another event, then we execute the later callback function.
if (callNow) func.apply(context, args);
};
}

A debounce is a higher-order function, which is a function that returns another function (named executedFunction here for clarity). This is done to form a closure around the func, wait, and immediate function parameters and the timeout variable on line 9 so that their values are preserved.

Here are the meaning of each variable:
func: The function that you want to execute after the debounce time
wait: The amount of time you want the debounce function to wait after the last received action before executing func.
immediate: This determines if the function should be called on the leading edge and not the trailing. This means you call the function once immediately and then sit idle until the wait period has elapsed after an action. After the idle time has elapsed, the next event will trigger the function and restart the debounce.
timeout: The value used to indicate a running debounce.

Usage of debounce

Common scenarios for a debounce are resize, scroll, and keyup/keydown events. In addition, you should consider wrapping any interaction that triggers excessive calculations or API calls with a debounce.

Here is an experiment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
function debounce(func, wait, immediate) {
// debounce function
}
function updateDisplay(e) {
console.log("called");
document.querySelector("#text-display").innerText = e.target.value;
}

window.onload=function () {
>>>>>>> a
document.querySelector("#text-input").addEventListener("input", debounce(updateDisplay, 400));
>>>>>> b
document.querySelector("#text-input").addEventListener("input", updateDisplay);
>>>>>> c
document.querySelector("#text-input").addEventListener("change", debounce(updateDisplay, 400));
}

</script>
</head>
<body>
<form>
<label>
Type your input:
<input type="text" id="text-input"/>
</label>
</form>

Display:
<p id="text-display">
</p>

</body>
</html>
  • Outcome of version a:
    As can be seen, only after I have stopped typing for 400ms will the display be updated.
  • Outcome of version b: As can be seen, the display is updated immediately.
  • Outcome of version c:
    As can be seen, the display is updated only when the input box loses focus.
    This has to do with the difference between input event and change event
    • oninput event occurs when the text content of an element is changed through the user interface. Triggers immediately, unlike change.
    • onchange occurs when the selection, the checked state or the contents of an element have changed. In some cases, it only occurs when the element loses the focus

Using react-debounce-input package for React debounce

There are issues related to using debounce in React as it uses event pooling (will get on that later).
The package react-debounce-input deals with debouncing events, and this is what I used for signup:

1
2
3
4
5
6
7
8
9
10
11
12
// import { DebounceInput } from 'react-debounce-input';
<DebounceInput
type='email'
name='email'
required={true}
debounceTimeout={400}
onChange={event => {
handleEmailInput(event);
checkEmailOccupied(event.target.value);
}}
value={userEmail}
/>