Write a debounce function from scratch.
A debounce needs one piece of state, the pending timer, and two actions: cancel the pending call when a new one arrives, and schedule the real call when the calls stop.
const debounce = (fn, waitMs) => {
let timer = null;
const debounced = (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn(...args);
}, waitMs);
};
return debounced;
};
// Try it.
const log = (value) => console.log("called with", value);
const debounced = debounce(log, 30);
debounced("a");
debounced("b");
debounced("c");
// Only "c" is logged, 30ms after the last call.The rest parameters matter. The debounced function accepts any arguments and forwards the ones from the final call, which is what you want for a search box, where the last keystroke is the one that matters. The this binding is another detail worth getting right, because a debounced method still needs its receiver, and an arrow function inside the returned wrapper is the simplest way to keep it.
Note the timer being reset to null inside the callback. Without that, the function would still hold the identifier of a timer that has already fired, and the next cancel would clear a timer that no longer exists. It is harmless but it is untidy, and it is the kind of thing that shows you are comfortable with the timer API.
That is the whole implementation. Around fifteen lines, and it is a good question because it tests whether you understand the timer API, closures and the difference between scheduling and cancelling work.