Skip to main content

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

useEffectuseLayoutEffect
Runs after browser paints (async)Runs before browser paints (sync)
Non-blockingBlocking — delays paint
99% of use casesDOM 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-Series03-react