Mark Lowel Montealto — Full Stack Developer & DevOps Engineer

Mark Lowel
Montealto

Full Stack Developer & DevOps Engineer

Blog

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

  • useEffect is the most misunderstood React hook — its mental model differs from componentDidMount/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-deps rule) 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 useEffect adds 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 effect

Mistake 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]) where triggerState is 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

SituationBetter alternative
Derive state from props/stateCompute during render
React to a user actionEvent handler
Transform data for displayuseMemo
Initialise a refuseRef with initial value
Fetch on user actionCall async in event handler

Conclusion

  • Most useEffect bugs 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.