Debouncing Explained
Note from the archive: this was originally published on typescript.wtf in 2023 and was marked as AI-generated in the source repository. I am preserving that provenance here.
Debouncing is a technique that can be used to limit the rate at which a function is executed. This is particularly useful when working with events that can fire at a high rate, such as scroll or resize events. In this blog post, we will be discussing how to implement debouncing in JavaScript and how it can be used to improve the performance of your code.
When working with events that can fire at a high rate, it can be beneficial to limit the rate at which a function is executed. This is where debouncing comes in. A debounced function will only be executed after a certain amount of time has passed since it was last called. This can prevent a function from being called multiple times in rapid succession, which can lead to poor performance and unexpected behavior.
Here is an example of a debounce function written in TypeScript:
function debounce<T extends (...args: any[]) => any>(fn: T, wait: number): T {
let timeoutId: number;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), wait);
} as any;
}
The debounce function takes two arguments: a function, fn, and a wait time, wait. The returned function, when called, will clear the previously set timeout and set a new one with the provided wait time. This means that if the returned function is called multiple times before the wait time has passed, only the last call will be executed.
To use the debounce function, we simply need to call it and pass in the function that we want to debounce and the wait time. Here’s an example of using the debounce function to debounce a handleResize function that is called on the resize event:
window.addEventListener("resize", debounce(handleResize, 250));
In this example, the handleResize function will only be executed once every 250 milliseconds, regardless of how many times the resize event is fired.
One important thing to note is that, when the debounced function is called, it will not be passed any arguments. This is because the debounced function does not execute until the wait time has passed, at which point the arguments may no longer be relevant. If you need to pass arguments to your debounced function, you can use closures to capture the arguments at the time the debounced function is called.
Unit Testing with Jest
describe("debounce function", () => {
let func: jest.Mock;
let debouncedFunc: (...args: any[]) => any;
beforeEach(() => {
func = jest.fn();
debouncedFunc = debounce(func, 100);
});
test("should call the debounced function only once within the wait time", () => {
debouncedFunc();
debouncedFunc();
debouncedFunc();
expect(func).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(99);
expect(func).toHaveBeenCalledTimes(0);
jest.advanceTimersByTime(1);
expect(func).toHaveBeenCalledTimes(1);
});
test("should call the debounced function with the correct arguments", () => {
const args = [1, 2, 3];
debouncedFunc(...args);
jest.advanceTimersByTime(100);
expect(func).toHaveBeenCalledWith(...args);
});
test("should call the debounced function with the correct context", () => {
const context = {};
debouncedFunc.call(context);
jest.advanceTimersByTime(100);
expect(func.mock.instances[0]).toBe(context);
});
test("should call the debounced function again if called after wait time", () => {
debouncedFunc();
jest.advanceTimersByTime(100);
debouncedFunc();
jest.advanceTimersByTime(100);
expect(func).toHaveBeenCalledTimes(2);
});
});
This set of tests cover the different scenarios that the debounce function should handle. The first test ensures that the debounced function is called only once within the wait time, even if it’s called multiple times. The second test ensures that the debounced function is called with the correct arguments. The third test ensures that the debounced function is called with the correct context, and the fourth test ensures that the debounced function is called again if it’s invoked after the wait time.
Note that in order for the timers to work correctly in the tests, you need to run jest.useFakeTimers() at the beginning of your test file, and jest.runAllTimers() or jest.advanceTimersByTime(time) if you want to move forward in time.
Using with React
In addition to improving performance, debouncing can also be useful when working with React forms. For example, when making an API call every time the user types into an input field, debouncing can be used to prevent the function from being called on every keystroke and instead wait for the user to finish typing before making the call. This can prevent overwhelming the server with too many requests and improve the overall user experience.
Here is an example of how you might use the debounce function with a React form and hooks:
import { useState, useEffect } from "react";
function SearchForm() {
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
useEffect(() => {
const debouncedFn = debounce(() => {
setDebouncedSearchTerm(searchTerm);
}, 500);
debouncedFn();
}, [searchTerm]);
function handleSearch(event) {
setSearchTerm(event.target.value);
}
return (
<form>
<input type="search" value={searchTerm} onChange={handleSearch} placeholder="Search..." />
</form>
);
}
In this example, the debounce function is used to debounce the API call that is triggered by the user typing into the input field. The debounce function is called every time the searchTerm state changes. This way, the API call is only made after the user has stopped typing for 500 milliseconds. This can improve the performance of the application and provide a better user experience.
Debouncing is a powerful technique that can be used to improve the performance and behavior of your code. It can be used to limit the rate at which a function is executed, which can prevent performance issues and unexpected behavior. With the implementation of debouncing, you can prevent the function from being called too frequently and so you can easily handle the high-frequency events. As demonstrated in this blog post, debouncing is not only useful for handling high-frequency events, but also for handling forms and API calls in React applications.