React Hooks — useEffect
Difficulty: 🟡 Medium | Frequency: ⭐⭐⭐⭐⭐ | Companies: Google, Meta, Amazon, Microsoft, Netflix
What is useEffect?
useEffect performs side effects in functional components — data fetching, subscriptions, DOM manipulation, timers, analytics. It runs after render, asynchronously (after browser paint).
Dependency Array
useEffect(() => { ... }); // Runs after every render
useEffect(() => { ... }, []); // Runs once on mount
useEffect(() => { ... }, [dep]); // Runs when dep changes
Core Patterns
// 1. Fetch on mount
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
// 2. Fetch when dependency changes
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
// 3. Cleanup: timers
useEffect(() => {
const interval = setInterval(() => setSeconds(s => s + 1), 1000);
return () => clearInterval(interval); // Runs on unmount
}, []);
// 4. Cleanup: event listeners
useEffect(() => {
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
// 5. Cleanup: WebSocket
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (e) => setMessages(prev => [...prev, e.data]);
return () => ws.close();
}, [roomId]);
Async/Await in useEffect
You can't make the useEffect callback itself async. Define an inner function:
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
try {
setLoading(true);
const data = await fetch(`/api/users/${userId}`).then(r => r.json());
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
};
fetchUser();
return () => { cancelled = true; };
}, [userId]);
Common Mistakes
// ❌ Missing dependency
useEffect(() => {
fetch(`/api/search?q=${query}`).then(r => r.json()).then(setResults);
}, []); // Bug: results never update when query changes
// ✅ Include all dependencies
}, [query]);
// ❌ No cleanup — memory leak
useEffect(() => {
const interval = setInterval(() => console.log('tick'), 1000);
}, []); // Interval runs forever!
// ✅ Always cleanup
return () => clearInterval(interval);
// ❌ Infinite loop: sets state that's in dependencies
useEffect(() => {
setCount(count + 1); // triggers re-render → effect runs → ...
}, [count]);
// ✅ Use functional update to avoid dependency
useEffect(() => {
setCount(c => c + 1); // no count dependency needed
}, []);
Separate Concerns — Multiple Effects
function UserDashboard({ userId }) {
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]); // Fetch user
useEffect(() => {
fetch(`/api/posts?userId=${userId}`).then(r => r.json()).then(setPosts);
}, [userId]); // Fetch posts (separate concern)
}
Cleanup Lifecycle
Mount:
render → DOM update → useEffect runs
Update (dep changed):
render → DOM update → CLEANUP previous effect → new useEffect runs
Unmount:
CLEANUP runs
useEffect vs useLayoutEffect
useEffect | useLayoutEffect |
|---|---|
| Runs after browser paints (async) | Runs before browser paints (sync) |
| Non-blocking | Blocking — delays paint |
| 99% of use cases | DOM measurements, prevent flicker |
Real-World Bug: Memory Leak in Chat
// ❌ WebSocket never closed when roomId changes
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (e) => setMessages(prev => [...prev, e.data]);
// No cleanup! Old connections accumulate
}, [roomId]);
// After 1 hour of room switching: 120 open WebSocket connections!
// ✅ Fixed
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (e) => setMessages(prev => [...prev, e.data]);
return () => ws.close(); // Closes old connection before new one opens
}, [roomId]);
React 18 Strict Mode
In development, React 18 Strict Mode double-invokes effects (mount → cleanup → mount) to surface missing cleanup. This catches bugs like duplicate event listeners:
// ❌ Missing cleanup — Strict Mode reveals 2 listeners
useEffect(() => {
window.addEventListener('resize', handler);
}, []);
// ✅ Strict Mode shows this works correctly
useEffect(() => {
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
Follow-up Questions
- What's the difference between useEffect and useLayoutEffect?
- How do you handle race conditions in useEffect?
- Can you explain the cleanup function lifecycle?
- How does React 18 Strict Mode affect useEffect?
- When should you use AbortController?
Content from Frontend-Master-Prep-Series — 03-react