The most common useEffect mistake isn't a dependency-array bug or a missing cleanup. It's using an effect to compute state from other state — mirroring a value into useState and syncing it with an effect, when you could just calculate it during render. I built a demo with live render counters that shows exactly what that costs.
▶ Live demo: https://derived-state.pages.dev/
Source: https://github.com/dev48v/derived-state
The anti-pattern
const [first, setFirst] = useState("");
const [last, setLast] = useState("");
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(first + " " + last); // 🐞
}, [first, last]);
Looks harmless. It isn't. Two things go wrong:
-
Two renders per keystroke. You type →
setFirstre-renders the component. Then the effect runs (after render) and callssetFullName, which re-renders it again. In the demo, typing a few characters pushes this panel's render count to roughly double the other's. -
A stale frame. Because the effect runs after render, there's one commit where the inputs show the new value but
fullNamestill holds the old one. Usually invisible, occasionally a real bug (you render, readfullName, and it's a keystroke behind).
The fix is to delete the effect
const [first, setFirst] = useState("");
const [last, setLast] = useState("");
const fullName = first + " " + last; // ✅ just derive it
One render per keystroke. Always correct. No duplicate state to keep in sync. If the derivation is genuinely expensive, memoize it — still not an effect:
const filtered = useMemo(() => items.filter(matches(query)), [items, query]);
The mental model
State should be the minimal set of things React can't recompute. Anything you can calculate from existing state or props is not state — it's a derivation, and derivations belong in the render body (or a useMemo), not in useState + useEffect.
The tell is dead simple: if an effect's entire body is setState from other state or props, delete it.
When you actually do need an effect
Effects exist to synchronize with systems outside React:
- fetching data (or better, a data library / framework loader)
- subscriptions (WebSocket, event emitter,
store.subscribe) - timers and intervals
- imperatively touching the DOM (focus, scroll, measuring, a non-React widget)
- sending analytics on a specific lifecycle moment
A few more things that feel like they need effects but don't:
- Responding to a user event (a click, a submit) → put the logic in the event handler.
-
Resetting a component's state when a prop changes → give it a
keyso React remounts it; don't watch the prop in an effect and reset by hand. - Transforming data for rendering → do it while rendering.
This all mirrors React's own You Might Not Need an Effect doc — the demo just makes the extra renders visible so the advice stops being abstract.
Type into both panels and watch one counter run away from the other. If it made you delete an effect somewhere, a star helps others find it: https://github.com/dev48v/derived-state
Top comments (0)