Know if you're actually ready. Take the React quiz → get your AI readiness report.Take the free test →
Study guide · React
React useEffect: Dependencies, Cleanup, and Common Pitfalls
useEffect is the most misunderstood React hook. Here's a clear guide to dependencies, cleanup, and the pitfalls that cause bugs.
useEffect fundamentals
useEffect runs side effects after every render, or conditionally based on dependencies.
import { useEffect } from 'react';
// Runs after EVERY render (no dependency array)
useEffect(() => {
document.title = `Count: ${count}`;
});
// Runs ONCE after mount (empty dependency array)
useEffect(() => {
fetchData();
}, []);
// Runs when count changes
useEffect(() => {
console.log('count changed to', count);
}, [count]);NoteThe dependency array controls WHEN the effect runs — not a performance optimisation. Put every value from the component scope that the effect uses.
Cleanup functions
Return a function from useEffect to clean up subscriptions, timers, and event listeners.
useEffect(() => {
const handler = () => console.log('scrolled');
window.addEventListener('scroll', handler);
// Cleanup: runs before the effect re-runs AND when component unmounts
return () => {
window.removeEventListener('scroll', handler);
};
}, []);
// Timer cleanup
useEffect(() => {
const id = setInterval(() => setCount(c => c + 1), 1000);
return () => clearInterval(id);
}, []);NoteWithout cleanup, event listeners and timers continue running after the component unmounts — a common source of memory leaks.
Fetching data in useEffect
Data fetching is the most common useEffect use case. Handle loading and error states properly.
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false; // prevent stale updates
async function load() {
try {
setLoading(true);
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
} catch (e) {
if (!cancelled) setError(e.message);
} finally {
if (!cancelled) setLoading(false);
}
}
load();
return () => { cancelled = true; };
}, [userId]); // re-fetch when userId changes
}The infinite loop trap
Missing or incorrect dependencies cause useEffect to run in an infinite loop.
// INFINITE LOOP: data is created on every render
// and changes reference each time
useEffect(() => {
setUser(data);
}, [data]); // data is a new object every render
// INFINITE LOOP: setting state that's in the dependency array
useEffect(() => {
setCount(count + 1); // triggers re-render
}, [count]); // which triggers effect again
// FIX: use functional update to remove count from deps
useEffect(() => {
setCount(prev => prev + 1); // no need for count in deps
}, []);NoteIf your effect causes an infinite loop, you've either set state from state that's in the deps array, or you're including an unstable reference (object/array) in the deps.
Exam tipThe most common useEffect interview question: "Why does this cause an infinite loop?" — usually because the dependency array contains a value that's recreated on every render (like an inline object or function), or because the effect sets state that's also in the deps array.
Want a person to help?
Struggling with React? Work 1:1 with a vetted React tutor on Wyzant. (affiliate link)
Find a React tutor →