useEffect & Side Effects
EasyuseEffect synchronizes a component with an external system — network requests, DOM manipulation, timers, or subscriptions. It runs after the browser paints, and its cleanup function runs before the next effect or on unmount. The dependency array controls when the effect re-runs.
Interactive Visualization
Key Points
- Runs after render and browser paint (asynchronous)
- Dependency array controls when the effect re-runs
- Empty dependency array [] means run only on mount
- No dependency array means run after every render
- Cleanup function runs before re-running the effect and on unmount
- Each render has its own effect with captured values from that render
- StrictMode in development runs effects twice to detect missing cleanups
Code Examples
Data Fetching with Cleanup
import { useState, useEffect } from 'react' interface User { id: string name: string } function UserProfile({ userId }: { userId: string }) { const [user, setUser] = useState<User | null>(null) const [loading, setLoading] = useState(true) useEffect(() => { const controller = new AbortController() setLoading(true) fetch(`/api/users/${userId}`, { signal: controller.signal }) .then((res) => res.json() as Promise<User>) .then((data) => { setUser(data) setLoading(false) }) .catch((err: Error) => { if (err.name !== 'AbortError') { setLoading(false) } }) return () => controller.abort() }, [userId]) if (loading) return <div>Loading...</div> return <div>{user?.name}</div> }
AbortController in the cleanup function cancels the previous fetch when userId changes, preventing race conditions
Event Listener Subscription
import { useState, useEffect } from 'react' function WindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, }) useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight, }) } window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) return <p>{size.width} x {size.height}</p> }
The empty dependency array ensures the listener is added once on mount and removed on unmount via the cleanup return
Common Mistakes
- Omitting dependencies that are used inside the effect, causing stale closures
- Adding object or function dependencies that change every render, causing infinite loops
- Forgetting the cleanup function for subscriptions, timers, or event listeners
Interview Tips
- Explain the mental model: effects synchronize with external systems, not lifecycle events
- Know how to prevent race conditions with AbortController or an ignore flag
- Discuss why useEffect runs after paint and when useLayoutEffect is needed instead