React Code Interview 005: Throttle, Debounce, and a Better Search Experience in React
Using Debounce and Throttle to Optimize Search Requests in React
Debounce and throttle often show up in frontend interviews — but they become truly valuable when you’re building responsive, data-driven UIs in the real world. In this issue, we’ll explore how to design a search experience that feels snappy to the user while protecting your backend from unnecessary load.
Background and Design Challenge
Imagine a search box that triggers a request every time the user types a character. Seems reasonable — but here’s what happens when a user types "hello"
:
h
→ triggers one requesthe
→ anotherhel
→ anotherhell
→ anotherhello
→ and another
That’s five requests for a single query. If each takes 300ms and they resolve out of order, your UI might flicker with stale results, or worse, show the wrong data altogether.
The challenge here is to reduce redundant requests, avoid race conditions, and keep the UI consistent — all while staying responsive to what the user is typing.
Let’s look at two patterns to help us do just that.
We’ll implement both debounce and throttle behavior in a custom useSearch
hook. Each approach solves the same problem — but with slightly different trade-offs.
1. Debounce: Wait Until the User Pauses
In the debounce version, we delay the request until 500ms after typing stops. If the user keeps typing, the delay resets. So in our "hello"
example:
Type
h
→ waitType
he
→ timer resetsType
hel
→ resets again…until the user finishes typing
After 500ms pause → one request for
"hello"
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
fetchResult(query);
}, 500);
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [query]);
Why it works:
Reduces request volume dramatically
Ensures the final intent is what gets sent
Keeps the user experience smooth and focused
But does it prevent race conditions?
One of my subscribers asked a sharp question on my video about request cancellation:
“Can debounce fix the race condition problem?”
The answer: not really.
While debounce reduces the number of requests, it doesn't control how long the server takes to respond. If a previous request is slow and resolves after a newer one, it can still overwrite the correct data in the UI. That’s why AbortController or manual result handling (e.g. latest-only logic) is still needed for true race safety.
2. Throttle + Debounce: Limit Rate, Then Catch Up
In this version, we allow the first request to go out right away, but we throttle subsequent requests to once every second. If the user keeps typing, a final request is debounced to ensure the latest query is eventually sent.
For the "hello"
input typed quickly:
h
→ request goes out immediatelye
,l
,l
,o
→ requests skippedAfter 1 second → one final request with
"hello"
useEffect(() => {
const now = Date.now();
const elapsed = now - lastCalled.current;
if (!query.trim()) {
setResults([]);
return;
}
if (elapsed > 1000) {
fetchResult(query);
lastCalled.current = now;
} else {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
lastCalled.current = Date.now();
fetchResult(query);
}, 1000 - elapsed);
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [query]);
Why it works:
Sends the first request fast to show early feedback
Limits the rate of follow-ups to avoid spamming
Always sends the final state — clean and predictable
But again: throttle manages when we send a request, not what order they resolve in. To prevent older responses from overwriting newer ones, we still need something like request cancellation.
If you prefer learning by watching, I’ve also created a video walkthrough where you can see the full example in action.
The key takeaway? Debounce and throttle help you control the flow of requests, but they don’t fully solve race conditions on their own. For that, combine these patterns with AbortController, or make sure you’re only applying results from the latest request.
Whether you’re preparing for interviews or building for production, understanding these layers of control will help you write code that’s not just functional — but reliable and robust.
If you enjoyed this challenge, check out the companion video where I walk through the implementation and compare all behaviors side-by-side with a real typing demo.
Subscribe for more frontend design patterns and clean code strategies: