useEffect is a React hook that runs side effects after render. It replaces the lifecycle methods from class components: componentDidMount, componentDidUpdate, and componentWillUnmount — all combined into one API.
How it works step-by-step:
Three dependency patterns:
1// 1. No deps array — runs after EVERY render2useEffect(() => {3 console.log("I run on every render");4});56// 2. Empty deps array — runs ONCE on mount (and cleanup on unmount)7useEffect(() => {8 console.log("I run once on mount");9 return () => console.log("I run on unmount");10}, []);1112// 3. With deps — runs when ANY dependency changes13useEffect(() => {14 console.log("count changed:", count);15}, [count]); // only runs when count changes
Cleanup functions are critical — they run:
Common pitfall 1 — Missing dependencies (stale closure):
1// BAD: count is "stale" — always shows initial value2useEffect(() => {3 const interval = setInterval(() => {4 console.log(count); // Always 0!5 }, 1000);6 return () => clearInterval(interval);7}, []); // Missing count dependency!89// GOOD: count is always current10useEffect(() => {11 const interval = setInterval(() => {12 console.log(count); // Current value13 }, 1000);14 return () => clearInterval(interval);15}, [count]);
Common pitfall 2 — Infinite loops:
1// BAD: setting state in effect without proper deps = infinite loop2useEffect(() => {3 setData(fetchedData); // triggers re-render → effect runs again4}, [data]); // data changes every render → infinite loop!56// GOOD: use a different dependency7useEffect(() => {8 const controller = new AbortController();9 fetch(url, { signal: controller.signal })10 .then(res => res.json())11 .then(setData);12 return () => controller.abort();13}, [url]); // Only runs when URL changes
Common pitfall 3 — Cleanup forgotten (memory leaks):
1// BAD: subscription never cleaned up2useEffect(() => {3 window.addEventListener("resize", handleResize);4 // Missing cleanup!5}, []);67// GOOD: properly cleaned up8useEffect(() => {9 window.addEventListener("resize", handleResize);10 return () => window.removeEventListener("resize", handleResize);11}, []);
ESLint rule: The react-hooks/exhaustive-deps ESLint plugin will warn you about missing dependencies. Always follow its suggestions — it catches the majority of useEffect bugs.
When NOT to use useEffect:
useMemo insteadkey prop instead