DevPath · Learn to code ESPTEN

Data and global state

You Might Not Need an Effect

The overuse of useEffect

useEffect is one of the most overused tools in React. Many people treat it as "code that runs when something changes", and put it everywhere. But an effect serves one specific thing: synchronizing your component with an external system (the network, the direct DOM, a setInterval, a subscription...). If you use it for something else, there's almost always a better option, simpler and bug-free.

Antipattern: "derived" state as state + effect

Classic case: you have firstName and lastName in state and you want the full name. The temptation is a third state synchronized with an effect:

// ❌ Unnecessary: redundant state + effect
function Form() {
  const [firstName, setFirstName] = useState("Ada");
  const [lastName, setLastName] = useState("Lovelace");
  const [full, setFull] = useState("");

  useEffect(() => {
    setFull(firstName + " " + lastName);
  }, [firstName, lastName]);
  // ...
}

This is worse in every way: there's an extra state, an extra render (the effect runs after painting, and setFull triggers another render) and a moment where full is out of sync.

The rule: if you can compute it from props/state, do it in the render

The full name is derived state: it's not new information, it's derived from what you already have. Compute it during the render, like a normal variable:

// ✅ Derived during the render
function Form() {
  const [firstName, setFirstName] = useState("Ada");
  const [lastName, setLastName] = useState("Lovelace");
  const full = firstName + " " + lastName; // recomputed on every render
  // ...
}

No effect, no extra state, no desync. This applies to filtering a list, counting elements, formatting a value, knowing whether something is empty... all of that is computed in the render. (If the computation is really expensive, wrap it in useMemo to avoid repeating it on every render; but useMemo is only an optimization, not a change of approach.)

Antipattern: event logic inside an effect

Another misuse: running logic that should go in an event handler.

// ❌ Reacting to a state change with an effect to "do something"
useEffect(() => {
  if (submitted) {
    showToast("Purchase completed!");
    emptyCart();
  }
}, [submitted]);

That logic happens because the user clicked "Buy", not because the component synchronizes with an external system. Its place is the handler:

// ✅ The interaction logic goes in the event handler
function buy() {
  sendOrder();
  showToast("Purchase completed!");
  emptyCart();
}

Rule of thumb: if something happens because the user did something (a click, a submit), it goes in the event handler. If something happens because the component appeared on screen and must synchronize with something external, it goes in an effect.

When DO you need an effect?

An effect is the right tool to synchronize with systems external to React:

// ✅ Synchronize with an external system: with cleanup
useEffect(() => {
  const id = setInterval(() => setSeconds((s) => s + 1), 1000);
  return () => clearInterval(id); // cleanup on unmount
}, []);

Before writing a useEffect, ask yourself: am I synchronizing with something external, or am I just trying to compute a value or respond to an interaction? In the latter two cases, You Might Not Need an Effect.

Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Global state: beyond ContextView the module →