React Code Interview #4: Handling Overlapping Requests in a Search Component
A Practical Introduction to Concurrency in React Using AbortController
Hello, everyone! Welcome to another installment of the React Code Interview series. Last time, we dived into infinite scrolling with React Query. This time, we’ll explore a deceptively simple scenario: adding a search feature to our application—and the subtle concurrency issues that can arise when multiple requests start overlapping.
Background
At first glance, a search box seems straightforward: a user types something, and our code sends an HTTP request with the current query. But users type quickly; they change their minds mid-search. In short, multiple requests can end up in flight at the same time, leading to potentially confusing or stale results.
Initial Implementation
Consider this basic useSearch
hook:
export function useSearch<T>() {
const [query, setQuery] = useState<string>("");
const [results, setResults] = useState<T[]>([]);
useEffect(() => {
if (!query.trim()) {
setResults([]);
}
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then((data) => setResults(data))
.catch(() => setResults([]));
}, [query]);
return { query, setQuery, results };
}
We update results
whenever query
changes. However, if the user types fast or the server is slow, multiple calls can resolve out of order. This can lead to stale or invalid data if a later query finishes before an earlier (but still in-flight) one.
Mocking the Backend
To showcase the issue, I mocked the backend using MSW (Mock Service Worker). Here’s a snippet:
http.get('/api/search', async ({ request }) => {
const url = new URL(request.url);
const query = url.searchParams.get('q')?.toLowerCase() || '';
// Introduce different delays to simulate real-world servers
await delay(query === 'java' ? 2000 : 200);
return HttpResponse.json(generateMockResults(query));
});
When the user searches for “java,” the request is artificially delayed for 2 seconds, whereas everything else is 200ms. You can imagine real-world scenarios where certain queries get routed by a load balancer to a more heavily loaded server, or the request hits a large dataset that takes longer to index—hence, “java” could end up slower than “javascript” or other queries. This introduces a race condition if “java” is still waiting while “javascript” arrives sooner.
Introducing AbortController
The solution is to cancel any ongoing request before issuing a new one. Enter AbortController
. Here’s our updated useSearch
hook:
export function useSearch<T>() {
const [query, setQuery] = useState<string>("");
const [results, setResults] = useState<T[]>([]);
const [controller, setController] = useState<AbortController | null>(null);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
// Abort any in-flight request before creating a new one
if (controller) {
controller.abort();
}
// Create a fresh AbortController
const newController = new AbortController();
setController(newController);
fetch(`/api/search?q=${query}`, { signal: newController.signal })
.then((res) => res.json())
.then((data) => setResults(data))
.catch(() => setResults([]));
// Clean up by aborting if the component unmounts
return () => newController.abort('unmount');
}, [query]);
return { query, setQuery, results };
}
How It Works
Check for an empty query: If the user hasn’t typed anything (or only spaces), we just clear the results.
Abort the previous request: If there is a running request, cancel it to avoid stale data updates.
Create a new
AbortController
: Tied to the current request, so we can clean it up at will.Cleanup: On unmount (or effect re-run), we abort any pending request to avoid state updates after unmount.
With these steps, only the most recent request “wins,” preventing old responses from overwriting new ones.
Summary: Outrunning Stale Requests
Implementing a simple search can get complicated when asynchronous requests collide. By using AbortController
, we ensure old requests are cancelled and cannot overwrite fresh data. It’s a small but crucial improvement for better user experience and clean code.
Special Note for Subscribers:
If you’re keen to learn more data fetching patterns I have released my eBook, React Data Fetching Patterns in leanpub! Use the coupon code pragmatic-developer at the link below for an exclusive discount:
https://leanpub.com/react-data-fetching-patterns/c/pragmatic-developer
Further Reading
React Data Fetching Patterns: A deep dive into advanced data-fetching strategies in React.
Advanced Network Patterns in React: Handling concurrency and error boundaries effectively.
Martin Fowler’s Article on Data Fetching in SPAs: Common pitfalls and best practices for data fetching in single-page applications.
Stay tuned for our next installment in the React Code Interview series! As always, feel free to reach out with any questions—and happy coding!