Skip to main content

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)