Understanding React useEffect: The Hook That Confuses Every Developer
Understanding React useEffect: The Hook That Confuses Every Developer
If you've worked with React for more than a week, you've encountered useEffect. You've probably also had it behave unexpectedly — running too many times, not running when you expected, or creating infinite loops.
This guide explains useEffect clearly, with examples of both correct and incorrect usage.
What useEffect Is For
useEffect lets you synchronize your component with external systems — APIs, the DOM, timers, subscriptions. It runs after the component renders and optionally cleans up after itself.
The conceptual model: "After this render, do this thing."
The Dependency Array
// No dependency array — runs after EVERY render
useEffect(() => { ... });
// Empty array — runs once, after the first render only
useEffect(() => { ... }, []);
// With dependencies — runs when dependencies change
useEffect(() => { ... }, [userId, filter]);
Common Mistake 1: Missing Dependencies
// ❌ Wrong — userId is used but not in dependency array
useEffect(() => {
fetchUser(userId);
}, []);
// ✅ Correct
useEffect(() => {
fetchUser(userId);
}, [userId]);
Common Mistake 2: Infinite Loops
// ❌ This creates an infinite loop
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(result => setData(result));
}); // No dependency array — runs after EVERY render
// ✅ Correct — fetches once on mount
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(result => setData(result));
}, []);
Common Mistake 3: Object and Function Dependencies
Objects and functions are recreated on every render. Including them as dependencies causes the effect to run every render.
// ✅ Option 1 — define inside effect
useEffect(() => {
const options = { method: 'GET' };
fetchData(options);
}, []);
// ✅ Option 2 — memoize
const options = useMemo(() => ({ method: 'GET' }), []);
useEffect(() => {
fetchData(options);
}, [options]);
The Cleanup Function
useEffect(() => {
const subscription = someAPI.subscribe(userId, handleUpdate);
return () => {
subscription.unsubscribe();
};
}, [userId]);
Without cleanup, subscriptions and timers continue running after the component unmounts — a common source of memory leaks.
Fetching Data the Right Way
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err.message);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId]);
When NOT to Use useEffect
React's documentation specifically calls out patterns where useEffect is the wrong tool:
- Transforming data for rendering: Do this during render, not in effects
- User event handling: Use event handlers, not effects
- Initializing the app: Run code outside components
Building a React project and running into issues? Get in touch — I'd be happy to help.