April 2025
·3 min read
7 useEffect Mistakes Every React Developer Should Stop Making
Missing dependencies, infinite loops, stale closures — the 7 most common useEffect pitfalls explained with before-and-after fixes.
Introduction
useEffectis the most misunderstood React hook — its mental model differs fromcomponentDidMount/componentDidUpdate.- React 19 + Strict Mode doubles effects in development to expose these bugs early.
- These 7 mistakes cover the most common issues in code reviews and stack overflow questions.
Mistake 1: Missing Dependencies in the Array
- The dependency array must list every reactive value read inside the effect.
- Omitting a dependency causes the effect to run with a stale snapshot of that value.
- Fix: run
eslint-plugin-react-hooks(exhaustive-depsrule) and trust the linter.
// Bug: count is read but not listed
useEffect(() => {
document.title = `Count: ${count}`;
}, []); // ❌ stale closure — never updates after first render
// Fix
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // ✅Mistake 2: Infinite Loops from Object/Array Dependencies
- Objects and arrays created inline are new references on every render → effect runs on every render.
- Fix: memoize with
useMemo/useCallback, or move the value outside the component.
// Bug: options is a new object every render
useEffect(() => {
fetchData(options);
}, [options]); // ❌ infinite loop
// Fix: memoize
const options = useMemo(() => ({ page, limit }), [page, limit]);
useEffect(() => {
fetchData(options);
}, [options]); // ✅Mistake 3: Using useEffect for Derived State
- Synchronising one state from another via
useEffectadds an extra render cycle. - Fix: compute the derived value during render (no effect needed).
// Bug: unnecessary effect + extra re-render
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);
useEffect(() => { setCount(items.length); }, [items]); // ❌
// Fix: derive directly
const count = items.length; // ✅ computed at render time, no effectMistake 4: Forgetting the Cleanup Function
- Effects that set up subscriptions, timers, or event listeners must return a cleanup.
- Without cleanup: memory leaks, stale event listeners, and "Can't perform state update on unmounted component" warnings.
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id); // ✅ cleanup
}, []);Mistake 5: Treating useEffect Like componentDidMount
- In React Strict Mode (development), effects fire twice on mount to surface cleanup bugs.
- If your effect can't run twice safely (e.g., it sends an analytics event), add a flag or use
useRef. - Rule: if the cleanup properly reverses the setup, double-firing is harmless.
useEffect(() => {
let active = true;
fetchUser(id).then(user => {
if (active) setUser(user); // ✅ guard for double-invoke / unmount race
});
return () => { active = false; };
}, [id]);Mistake 6: Async Functions Directly in useEffect
useEffect(async () => {...})returns a Promise, not a cleanup function — React ignores it silently.- Fix: define an inner async function and call it, returning the cleanup separately.
// Bug
useEffect(async () => {
const data = await fetchData(); // ❌ React ignores the returned Promise
setData(data);
}, []);
// Fix
useEffect(() => {
let active = true;
async function load() {
const data = await fetchData();
if (active) setData(data);
}
load();
return () => { active = false; }; // ✅ proper cleanup
}, []);Mistake 7: Overusing useEffect Instead of Event Handlers
- Effects run after render — for user interaction responses (button clicks, form submits), use event handlers instead.
- Effects are for synchronisation with external systems, not for reacting to UI events.
- Common symptom:
useEffect(() => { doSomething(); }, [triggerState])wheretriggerStateis set in a click handler.
// Bug: effect triggered by UI state
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) submitForm(data); // ❌ roundabout
}, [submitted, data]);
// Fix: put logic directly in the handler
function handleSubmit() {
submitForm(data); // ✅
}Quick Reference: When NOT to Use useEffect
| Situation | Better alternative |
|---|---|
| Derive state from props/state | Compute during render |
| React to a user action | Event handler |
| Transform data for display | useMemo |
| Initialise a ref | useRef with initial value |
| Fetch on user action | Call async in event handler |
Conclusion
- Most
useEffectbugs stem from the mismatch between "lifecycle" thinking and "synchronisation" thinking. - Trust the exhaustive-deps ESLint rule — if it asks you to add a dependency, the fix is rarely to suppress it.
- When React 19's compiler (
react-compiler) is stable in your stack, many of these become non-issues — but understanding the model still matters.