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:
- Subscribing to a data source (a WebSocket, an external
store, a browser event) and unsubscribing on unmount. - Manually controlling a browser or DOM API (measuring a node,
focusing an input, starting/stopping a
setInterval). - Triggering network requests that must happen when the component appears (although for this, the ideal is a data fetching library like TanStack Query, which wraps the effect for you).
// ✅ 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.