useEffect Cleanup and Dependencies
Answer
useEffect is React's hook for side effects. Understanding its cleanup function and dependency array is crucial for avoiding bugs like memory leaks and stale closures.
useEffect Structure
Dependency Array Behavior
// 1. No array - runs after EVERY render
useEffect(() => {
console.log("Runs every render");
});
// 2. Empty array - runs ONCE on mount
useEffect(() => {
console.log("Runs once on mount");
}, []);
// 3. With dependencies - runs when dependencies change
useEffect(() => {
console.log("Runs when count changes");
}, [count]);
Cleanup Function
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Effect: Start timer
const interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
// Cleanup: Clear timer
return () => {
clearInterval(interval);
};
}, []); // Only on mount/unmount
return <span>{seconds}s</span>;
}
When Cleanup Runs
Common Patterns
Event Listeners
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Subscribe
window.addEventListener("resize", handleResize);
handleResize(); // Initial size
// Cleanup: Unsubscribe
return () => window.removeEventListener("resize", handleResize);
}, []);
return (
<p>
{size.width} x {size.height}
</p>
);
}
Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let isCancelled = false; // Cleanup flag
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (!isCancelled) {
// Prevent update on unmounted component
setUser(data);
setLoading(false);
}
} catch (error) {
if (!isCancelled) {
setLoading(false);
}
}
}
fetchUser();
// Cleanup: Mark as cancelled
return () => {
isCancelled = true;
};
}, [userId]); // Re-fetch when userId changes
if (loading) return <p>Loading...</p>;
return <p>{user?.name}</p>;
}
WebSocket Connection
function Chat({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const socket = new WebSocket(`wss://chat.example.com/${roomId}`);
socket.onmessage = (event) => {
setMessages((prev) => [...prev, JSON.parse(event.data)]);
};
// Cleanup: Close connection
return () => {
socket.close();
};
}, [roomId]); // Reconnect when room changes
return <MessageList messages={messages} />;
}
Dependency Mistakes
// ❌ Missing dependency - stale closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always uses initial count!
}, 1000);
return () => clearInterval(interval);
}, []); // Missing count!
}
// ✅ Fix 1: Add dependency
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, [count]); // But creates new interval each time!
// ✅ Fix 2: Use functional update
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1); // Always uses current value
}, 1000);
return () => clearInterval(interval);
}, []); // No dependency needed!
Object/Array Dependencies
// ❌ Object created inline - effect runs every render!
useEffect(() => {
fetchData(options);
}, [{ limit: 10, sort: "asc" }]); // New object each render
// ✅ Fix: Use primitive values or useMemo
const options = useMemo(
() => ({ limit: 10, sort: "asc" }),
[] // Or actual dependencies
);
useEffect(() => {
fetchData(options);
}, [options]); // Stable reference
ESLint Dependencies Rule
// eslint-plugin-react-hooks warns about missing deps
useEffect(() => {
document.title = `Count: ${count}`;
}, []); // ⚠️ Warning: missing 'count' dependency
// Fix: Add the dependency
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // ✅ No warning
Key Points
- Empty array
[]= effect runs once on mount - Cleanup runs before next effect and on unmount
- Always include all values used in effect in deps
- Use functional updates to avoid dependency issues
- Cancel async operations in cleanup
- Objects/arrays need stable references (useMemo)