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)