mediumFrontend EngineerRide-sharing
Explain React's useEffect cleanup function and common race condition scenarios in data fetching
Posted 18/04/2026
by Mehedy Hasan Ador
Question Details
At a ride-sharing company, the interviewer presented:
"Our search page fetches results as the user types. Sometimes old results appear after newer ones. Here's the code:"
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => setResults(data));
}, [query]);
return <ResultList results={results} />;
}
"Type 'abc' then quickly change to 'xyz'. Sometimes 'abc' results show after 'xyz' results. Why?"
Suggested Solution
The Race Condition
Typing 'abc' → fetch('abc') starts → takes 800ms
Typing 'xyz' → fetch('xyz') starts → takes 200ms
→ 'xyz' returns first → setResults(xyz data) ✅
→ 'abc' returns later → setResults(abc data) ❌ OVERWRITES!
The older, slower request overwrites the newer results.
Fix 1: Cleanup with AbortController
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(data => setResults(data))
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort(); // Cancel previous request
}, [query]);
return <ResultList results={results} />;
}
Fix 2: Boolean Flag
useEffect(() => {
let cancelled = false;
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => {
if (!cancelled) setResults(data);
});
return () => { cancelled = true; };
}, [query]);
Fix 3: use() hook with Suspense (React 19+)
function SearchResults({ query }) {
const results = use(fetch(`/api/search?q=${query}`));
return <ResultList results={results} />;
}
// React automatically handles cancellation via Suspense boundaries
Other Common useEffect Cleanup Scenarios
| Scenario | What to Clean Up |
|---|---|
| Event listeners | removeEventListener() |
| Intervals/Timeouts | clearInterval() / clearTimeout() |
| WebSocket | ws.close() |
| Subscriptions | unsubscribe() |
| Third-party libs | Destroy instances (.destroy()) |
Rule of Thumb
If your effect creates something that persists after it runs, you need a cleanup function. Ask: "What would leak if this component unmounted right now?"