DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on • Originally published at github.com

You might not need an effect: syncing derived state with useEffect costs you a render (and a stale frame)

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]);
Enter fullscreen mode Exit fullscreen mode

Looks harmless. It isn't. Two things go wrong:

  1. Two renders per keystroke. You type → setFirst re-renders the component. Then the effect runs (after render) and calls setFullName, which re-renders it again. In the demo, typing a few characters pushes this panel's render count to roughly double the other's.
  2. A stale frame. Because the effect runs after render, there's one commit where the inputs show the new value but fullName still holds the old one. Usually invisible, occasionally a real bug (you render, read fullName, 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
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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 key so 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)