useEffect runs side effects after React commits DOM updates: fetching data, subscriptions, timers, and syncing with non-React systems. Cleanup functions prevent leaks when dependencies change or components unmount.
Effect Lifecycle
useEffect(callback, deps) runs the callback after paint. Return a cleanup function from the callback to run before the next effect or on unmount.
An empty dependency array [] runs once on mount (and cleanup on unmount). Omitting deps runs after every render — rarely what you want.
AbortController cancels in-flight fetch when userId changes or component unmounts.
Dependency Array Guide
useEffect(fn) // every render (avoid)
useEffect(fn, []) // mount + unmount only
useEffect(fn, [a, b]) // when a or b change
Include every reactive value from component scope used inside the effect.
Exhaustive deps rule from ESLint prevents stale effect bugs.
Cleanup runs before re-running effect and on unmount.
Prefer event handlers over effects for user-triggered logic.
useEffect Use Cases
When useEffect is — and is not — appropriate.
Use Case
Example
Fetch on mount/dep change
API call with abort cleanup
Subscriptions
WebSocket, event listeners
Timers
setInterval with clearInterval cleanup
DOM sync
Document title, third-party widgets
NOT for derived state
Compute during render instead
NOT for user clicks
Use event handlers
Effects vs Event Handlers
If something should happen because the user clicked a button, use onClick. If something should happen because userId prop changed, use useEffect. Misusing effects for click logic creates unnecessary runs and harder-to-follow code.
Replacing Fetch Effects with TanStack Query
Manual fetch + effect + loading state is repetitive. TanStack Query (React Query) handles caching, refetching, and deduplication — see the React Query lesson.