- React
- Core React Hooks: State, Synchronization, and Refs
- Effect Cleanups & Race Conditions
Effect Cleanups & Race Conditions
Race conditions occur when asynchronous requests resolve out of order (e.g., Query A sent before Query B, but B finishes before A). In useEffect, race conditions are solved using an active/ignore boolean flag or by canceling the in-flight request via AbortController in the cleanup function.
Key Learning Objectives
Diagnose network race conditions in React search and pagination components.
Implement the boolean flag cleanup pattern (ignore flag) to discard stale async responses.
Implement network request cancellation using AbortController and abortSignal.
Explain why updating state on an unmounted component was flagged in older React versions.
The Interview Problem & Context
Implement an asynchronous search fetch hook in useEffect that cancels or ignores outdated in-flight requests when the search query changes rapidly.
1import { useState, useEffect } from 'react';23export function useSearchResults(query: string) {4 const [results, setResults] = useState<string[]>([]);5 const [loading, setLoading] = useState(false);67 useEffect(() => {8 // Edge case: empty query9 if (!query) {10 setResults([]);11 return;12 }1314 // 1. Create AbortController to cancel previous network request15 const controller = new AbortController();16 let isIgnored = false;1718 async function fetchResults() {19 setLoading(true);20 try {21 const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {22 signal: controller.signal,23 });24 const data = await response.json();2526 // 2. Prevent setting state if query changed during flight!27 if (!isIgnored) {28 setResults(data);29 }30 } catch (err: any) {31 if (err.name !== 'AbortError' && !isIgnored) {32 console.error('Fetch error:', err);33 }34 } finally {35 if (!isIgnored) {36 setLoading(false);37 }38 }39 }4041 fetchResults();4243 // 3. Cleanup: executes when query changes or component unmounts44 return () => {45 isIgnored = true;46 controller.abort(); // Cancels the in-flight HTTP request!47 };48 }, [query]);4950 return { results, loading };51}
Deep Architectural Explanation
Anatomy of a Network Race Condition
Consider a search input:
- User types
"c"-> Request 1 dispatched (takes 1000ms). - User types
"ca"-> Request 2 dispatched (takes 200ms). - Request 2 finishes at 200ms: UI shows results for
"ca". - Request 1 finishes at 1000ms: Overwrites state with results for
"c"!
The user is left looking at results for"c"even though the input box says"ca".
The Two Architectural Solutions
- The Boolean Ignore Flag Pattern:
useEffect(() => {
let ignore = false;
asyncFetch(id).then(res => {
if (!ignore) setData(res);
});
return () => { ignore = true; };
}, [id]);- Simple and bulletproof against state corruption.
- However, the HTTP request still downloads data in the background over the wire.
- The
AbortControllerPattern:
const controller = new AbortController();
fetch(url, { signal: controller.signal });
return () => controller.abort();- Cancels the HTTP request immediately at the browser network layer, saving bandwidth and mobile battery life.
- Remember to catch and ignore
err.name === 'AbortError'.
Common Interview Traps & Pitfalls
Not catching AbortError, leading to uncaught promise rejection errors in console.
Setting state on unmounted components without an active/ignore flag.
Reusing the same AbortController instance across multiple renders instead of instantiating a new one per effect execution.
Senior Interviewer Follow-Up Probes
Why is dedicated server-state caching (TanStack Query / SWR) preferred over raw useEffect fetching in production?
How does React 19 use() API handle promise cancellation and caching under Suspense?
What happens if an aborted fetch request hits a Service Worker cache?
Frequently Asked Interview Questions
How does an ignore flag in useEffect solve race conditions?
When dependencies change, the cleanup function runs and sets ignore = true. When the older asynchronous promise resolves, it checks the flag and discards the result.
What is the advantage of AbortController over a simple boolean flag?
AbortController stops the HTTP request over the network, freeing up browser socket connections and saving network bandwidth, whereas a boolean flag still downloads the response.
How do you prevent "AbortError" from cluttering the console?
Wrap the fetch in a try/catch block and check if (error.name === "AbortError"). If true, ignore the error because it was intentionally cancelled.
Why does React 18 not warn about "Can't perform a React state update on an unmounted component"?
The React team removed that warning in React 18 because setting state on unmounted components does not cause a memory leak in JavaScript, though handling race conditions remains essential for correctness.
