Cleanup and Race Conditions
AdvancedEvery subscription needs a cleanup, and every fetch inside an effect needs to handle a response that arrived too late. Both are invisible in development and obvious to users.
Overview
The cleanup function is the part beginners skip, because everything appears to work without it. What breaks is subtler: listeners accumulate as the component mounts and unmounts, timers keep firing against a component that no longer exists, and — the one users actually notice — a slow response from an old request overwrites the result of a newer one. Typing quickly in a search box makes this happen reliably. Cleanup is how React lets you express "this connection is no longer wanted", and the discipline is to write it in the same keystroke as the setup.
Cleanup Runs More Than You Think
Before every re-run and on unmount — not only when the component disappears.
useEffect(() => {
const id = setInterval(tick, 1000)
return () => clearInterval(id) // write this immediately
}, [])
// Order when a dependency changes from A to B:
// cleanup(A) -> setup(B)
// So the effect is always torn down before it is set up again.
// Anything you START, you must STOP:
setInterval / setTimeout -> clearInterval / clearTimeout
addEventListener -> removeEventListener
new EventSource / WebSocket -> .close()
observer.observe -> observer.disconnect()
a library's init() -> its destroy()
// Without cleanup: the handler still holds the old props in its
// closure, keeps the component in memory, and fires setState on
// something unmounted — a leak, plus stale behaviour.The Race Condition
The bug you will actually ship. Two requests, and the slow one wins.
// Broken — types "re", "rea", "react"; whichever resolves last wins
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults)
}, [query])
// Results for "re" can land after results for "react".
// Fixed with an ignore flag
useEffect(() => {
let ignore = false
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(data => { if (!ignore) setResults(data) })
return () => { ignore = true } // this render's response is stale
}, [query])
// Or with AbortController, which also stops the request itself
useEffect(() => {
const c = new AbortController()
fetch(url, { signal: c.signal })
.then(r => r.json())
.then(setResults)
.catch(e => { if (e.name !== 'AbortError') setError(e) })
return () => c.abort()
}, [url])
// This is one of the strongest arguments for a data library:
// TanStack Query handles it for you, correctly, every time.StrictMode Surfaces Missing Cleanup
Development mounts every component twice on purpose, and it is doing you a favour.
// In development, React 18+ StrictMode runs:
// setup -> cleanup -> setup
// on the first mount of every component.
// So a missing cleanup shows up immediately as:
// - two WebSocket connections
// - a doubled analytics event
// - two fetches in the network tab
// The instinct is to add a "hasRun" ref to suppress it. Don't —
// that hides a real bug that will reappear in production the first
// time the component remounts (a route change, a filter switch).
// Correct: make the effect resilient to being run twice.
// An effect with proper cleanup already is.
// Note that this double-invoke happens ONLY in development.
// Production mounts once.Key Points to Remember
- 1Cleanup runs before every re-run of the effect and on unmount, not only when the component disappears
- 2Anything started in an effect — timer, listener, socket, observer — must be stopped in its cleanup
- 3Two requests from a changing dependency can resolve out of order; an ignore flag or AbortController fixes it
- 4StrictMode double-invokes effects in development to expose missing cleanup — fix the effect, do not suppress it
- 5A correctly cleaned-up effect is already safe to run twice, which is the real test
Interview Questions
Sign in to ask AriaDescribe a race condition caused by fetching inside useEffect and how to fix it.
When does an effect's cleanup function run?
Why does React StrictMode mount components twice in development?
Ask Aria about Cleanup and Race Conditions
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.