Skip to content
C Codeloom
JavaScript

React useEffect Common Mistakes

Avoid the most common useEffect mistakes — missing dependencies, infinite loops, stale closures, and effects that should not be effects in the first place.

·4 min read · By Codeloom
Beginner 10 min read

What you'll learn

  • Why most things should not be effects
  • How to read the dependency array correctly
  • Avoiding infinite loops
  • Handling cleanup and race conditions
  • When to use refs instead of state in effects

Prerequisites

  • Familiarity with JavaScript basics

The useEffect hook is one of the most powerful tools in React, but it is also one of the easiest to misuse. Effects are an escape hatch into the world outside React, and using them incorrectly leads to bugs that are hard to track down. This tutorial walks through the most common mistakes.

Using Effects for Derived State

The first mistake is using useEffect to compute a value that depends on props or state. Effects run after render, so you cause an unnecessary extra render and a flash of stale UI.

// Wrong
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// Right
const fullName = `${firstName} ${lastName}`;

If derivation is expensive, wrap it in useMemo. Either way, do not push it into state via an effect.

Missing Dependencies

The dependency array tells React when to rerun the effect. Leaving out a value that the effect uses leads to stale closures, where the effect sees old props or state forever.

useEffect(() => {
  const id = setInterval(() => console.log(count), 1000);
  return () => clearInterval(id);
}, []); // Bug: count is captured once

Enable the react-hooks/exhaustive-deps ESLint rule. It catches missing dependencies automatically and saves countless hours of debugging.

Infinite Loops

When the dependency array includes a value that the effect modifies, you get an infinite loop. The classic version is setting state inside an effect that depends on that state.

useEffect(() => {
  setUser({ ...user, lastSeen: Date.now() });
}, [user]); // user changes, effect reruns, user changes...

Either remove the dependency, use a functional updater, or move the logic out of an effect entirely.

Object and Array Dependencies

Objects and arrays compared by reference can also cause loops. Inline literals are a new reference every render.

// Bad
useEffect(() => {
  fetchData(options);
}, [{ id: 1 }]); // New object each render!

Move stable values outside the component, or memoize them with useMemo. For primitive ids, prefer passing the id rather than the whole object.

Forgetting Cleanup

Many effects need cleanup — subscriptions, timers, event listeners, abort controllers. Return a cleanup function from the effect.

useEffect(() => {
  const onResize = () => setWidth(window.innerWidth);
  window.addEventListener('resize', onResize);
  return () => window.removeEventListener('resize', onResize);
}, []);

Without cleanup, you leak listeners and memory. In React 18 development mode, effects run twice on mount, which surfaces missing cleanup right away.

Race Conditions in Async Effects

When an effect fires off a request, a later render can fire off another. If the second response arrives first, you can show stale data.

useEffect(() => {
  let cancelled = false;
  async function load() {
    const data = await fetch(`/users/${id}`).then(r => r.json());
    if (!cancelled) setUser(data);
  }
  load();
  return () => { cancelled = true; };
}, [id]);

The cancelled flag prevents the late response from updating state. An AbortController is even better when the network layer supports it.

Effects That Should Be Event Handlers

If something should happen in response to a user action, do it in the event handler — not in an effect that listens to state changes. Effects exist for synchronization with external systems, not for reacting to clicks.

// Wrong: showing a toast when the count crosses a threshold via effect
useEffect(() => {
  if (count === 10) toast('Reached 10');
}, [count]);

// Right
function handleClick() {
  const next = count + 1;
  setCount(next);
  if (next === 10) toast('Reached 10');
}

The handler version runs exactly when the user clicked, not at some indirect later time.

Reading the Latest State with Refs

Sometimes inside an interval or async callback you want the latest state without recreating the effect. Refs let you read mutable values without affecting render.

const countRef = useRef(count);
useEffect(() => { countRef.current = count; });

useEffect(() => {
  const id = setInterval(() => console.log(countRef.current), 1000);
  return () => clearInterval(id);
}, []);

This pattern keeps the timer alive across rerenders while still reading fresh values.

Wrapping Up

Most useEffect bugs trace back to a few categories — using effects when you do not need them, lying about dependencies, or forgetting cleanup. Start by asking whether an effect is really required, then make the deps honest, and your React code will be far calmer.