Debounce Function
Wait until the user stops typing (or resizing, or scrolling) before doing expensive work — with a cancel method.
Live preview Open in Playground
/**
* Returns a version of `fn` that only runs after the calls
* have stopped for `wait` milliseconds.
*/
export function debounce(fn, wait = 300) {
let timeoutId;
function debounced(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = undefined;
fn.apply(this, args);
}, wait);
}
// Let callers throw away a pending call (e.g. when a component unmounts)
debounced.cancel = () => {
clearTimeout(timeoutId);
timeoutId = undefined;
};
return debounced;
}An input event fires on every keystroke. If each one triggers a search, typing "kuala lumpur" sends 12 searches. Debouncing says: restart a timer on every call, and only run when the timer finishes. Typing quickly keeps restarting the timer, so the function runs once — after the pause.
Line by line
let timeoutIdlives in a closure: every call todebouncedsees the same variable, even thoughdebounceitself finished running long ago.clearTimeout(timeoutId)cancels the previous pending run. Calling it withundefinedis safe — it simply does nothing....argscollects whatever arguments were passed (like the event), andfn.apply(this, args)passes them on with the samethis. That's whydebouncedis a normalfunction, not an arrow function — arrows don't get their ownthis.debounced.cancelis a property on the function (functions are objects in JavaScript). Call it when the work is no longer wanted.
Debounce vs throttle
Debounce runs once after activity stops (search boxes, auto-save). Throttle runs at most once every N ms during activity (scroll position, drag). Different tools for different jobs.
Usage
import { debounce } from "./debounce.js";
const search = document.querySelector("#search");
const runSearch = debounce((event) => {
console.log("Searching for:", event.target.value);
}, 400);
search.addEventListener("input", runSearch);
// Later, if the search box is removed:
// runSearch.cancel();