Prompt
Implement debounce(fn, wait, options):
- Trailing by default:
fnruns once,waitms after the last call { leading: true }:fnfires immediately on the first call, then ignores calls until quiet- The returned function exposes
.cancel()to drop any pending invocation - Arguments and
thisof the last call are forwarded tofn
This is the search-box / resize-handler primitive — write it like you'd ship it.
Solution
Debounce exists to convert a stream of events into one decision — without it, a search box fires a request per keystroke and the last response to arrive (not the last query typed) wins the race. That's the why an interviewer wants first; the timer mechanics are secondary.
The design decisions that read as senior:
- The timer variable is the entire state machine.
timer === nullmeans "quiet period over" — that one check implements the leading edge. No booleans, no timestamps. .cancel()is not optional polish. In React, a debounced callback that survives unmount callssetStateon a dead component. The cleanup story (useEffectreturn calling.cancel()) is part of the API, which is why lodash and use-debounce both ship it.fn.apply(this, args)forwards the last call's arguments — with a plainfn()the search box debounces correctly but searches for an empty string.
Red flag: confusing debounce with throttle. Debounce = "wait for silence" (search input); throttle = "at most once per interval" (scroll position). Saying "I'd debounce the scroll handler" ships a UI that only updates when the user stops scrolling.
Say it: "Debounce collapses a burst of events into the final one after quiet time; the leading option trades freshness for immediacy, and cancel is what makes it safe to use inside a component lifecycle."