You Might Not Need an Effect
Effects are an escape hatch for synchronizing React with systems outside of React — the DOM, the network, browser APIs, third-party widgets. They are not a general-purpose “run some code when state changes” tool, and reaching for them by reflex is one of the most common sources of bugs in React apps. Many effects can be deleted entirely once you recognize the pattern: data you can compute during render should be computed during render, and logic triggered by a user action belongs in an event handler. This page catalogs the recurring “you might not need an effect” cases and the cleaner fix for each.
Why effects are often the wrong tool
An effect runs after render, which means any state you set inside one triggers a second render. If that second render computes layout-affecting values, the user can see a flash of stale content. Effects also re-run whenever their dependencies change, so it is easy to create redundant work, render loops, or subtle race conditions. The mental rule of thumb: if you are not synchronizing with an external system, you probably do not need an effect.
| Symptom | Likely fix |
|---|---|
| Setting state from another piece of state | Compute it during render |
useEffect that runs after a click or submit | Move the logic into the event handler |
| Clearing state when a prop changes | Reset with a key |
| Caching an expensive calculation | useMemo, not state + effect |
Transforming data for render
The most frequent anti-pattern is storing a value in state that is fully derived from existing props or state, then keeping it “in sync” with an effect.
// Anti-pattern: redundant state + effect
function Cart({ items }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0));
}, [items]);
return <p>Total: ${total}</p>;
}
The total is always recoverable from items, so the effect just causes an extra render. Compute it during render instead:
// Fix: derive during render
function Cart({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0);
return <p>Total: ${total}</p>;
}
If the calculation is genuinely expensive, memoize it — but still no effect and no state:
function Cart({ items }) {
const total = useMemo(
() => items.reduce((sum, item) => sum + item.price, 0),
[items]
);
return <p>Total: ${total}</p>;
}
Reach for
useMemoonly after measuring. Most reductions and filters over reasonably sized lists are far cheaper than the render they live in, and premature memoization adds noise.
Responding to events
Code that runs because the user did something belongs in the event handler, not in an effect that watches state.
// Anti-pattern: effect reacting to a state flag
function CheckoutForm({ onSuccess }) {
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
showToast("Order placed!");
onSuccess();
}
}, [submitted, onSuccess]);
return <button onClick={() => setSubmitted(true)}>Place order</button>;
}
This indirection makes the flow hard to follow and re-runs whenever onSuccess changes identity. The toast is a consequence of a specific click, so handle it there:
// Fix: do it in the handler
function CheckoutForm({ onSuccess }) {
function handleSubmit() {
showToast("Order placed!");
onSuccess();
}
return <button onClick={handleSubmit}>Place order</button>;
}
The question to ask: did this happen because the component rendered/displayed, or because the user did something? Display logic → effect. User action → handler.
Resetting state when a prop changes
Sometimes you want to clear local state when a key piece of input changes — for example, resetting a comment draft when navigating to a different profile.
// Anti-pattern: clearing state in an effect
function ProfileEditor({ userId }) {
const [draft, setDraft] = useState("");
useEffect(() => {
setDraft("");
}, [userId]);
return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
This works but renders once with the old draft before clearing it. The idiomatic fix is to give the component a key. When the key changes, React unmounts the old instance and mounts a fresh one with all state reset — no effect required.
// Fix: reset via key from the parent
function ProfilePage({ userId }) {
return <ProfileEditor key={userId} userId={userId} />;
}
function ProfileEditor({ userId }) {
const [draft, setDraft] = useState("");
return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />;
}
Syncing derived state across components
When two components need the same derived value, do not duplicate it into state and synchronize with effects. Lift the source of truth up and derive in each place during render.
// Fix: single source of truth, derive on read
function SearchPage() {
const [query, setQuery] = useState("");
const [allItems] = useState(loadItems);
const visible = allItems.filter((i) =>
i.name.toLowerCase().includes(query.toLowerCase())
);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultCount count={visible.length} />
<ResultList items={visible} />
</>
);
}
There is no filteredItems state and no effect keeping it current — visible is recomputed from query on every render, which is exactly what you want.
Output:
Typing "re" → list and count update in the same render,
no flash of stale results, no effect re-runs.
Best Practices
- Before writing
useEffect, ask: am I synchronizing with an external system? If not, you probably do not need it. - Compute values from props and state during render; store in state only what cannot be derived.
- Put logic that responds to a specific user action in the event handler, not in an effect watching a flag.
- Use a
keyto reset component state when an identifying prop changes, instead of clearing it in an effect. - Keep a single source of truth and derive everywhere else on read — avoid mirroring state with effects.
- Use
useMemo(not state + effect) for expensive calculations, and only after measuring a real cost. - When you do keep an effect, make sure its dependencies and cleanup are correct so it only runs when truly needed.