useEffect is where a lot of React bugs are born & it's not because the hook is broken. It's because it's the tool people reach for whenever they're not sure where something should go. Need to do a thing when a value changes? useEffect. Need to keep two pieces of state in sync? useEffect. It becomes the junk drawer of the component.
The trouble is that an effect runs after render, as a side effect of it. So the moment you use one to compute what you're about to display, you've added an extra render, a chance for stale data & a dependency array to get wrong. Most of the effects I delete in review fall into three buckets. Here they are, worst first.
You don't need an effect to transform data for rendering
This is the big one. You have some state & you want a derived value from it, so you store the derived value in more state and use an effect to keep it updated.
function Profile({ first, last }) {
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(first + " " + last);
}, [first, last]);
return <h1>{fullName}</h1>;
}Walk through what this actually does. The component renders with an empty fullName. Then the effect runs, calls setFullName & triggers a second render with the right value. So every prop change costs two renders & for one frame fullName is out of date. All of that to compute a string.
If you can calculate it from things you already have, calculate it during render. No state, no effect:
function Profile({ first, last }) {
const fullName = first + " " + last;
return <h1>{fullName}</h1>;
}It's simpler, it's always correct & there's no in-between render where the name is wrong. If the calculation is genuinely expensive — not string concatenation, but something heavy over a big list — wrap it in useMemo. That's still computing during render; it just skips the work when the inputs haven't changed. What you almost never need is to store derived data in state.
If you can compute a value from state you already have, compute it during render. Copying it into more state and syncing it with an effect just buys you an extra render and a chance to be stale.
You don't need an effect to reset state when a prop changes
Here's a classic. A profile page needs to clear its edit form whenever you navigate to a different user.
function Editor({ userId }) {
const [draft, setDraft] = useState("");
useEffect(() => {
setDraft(""); // reset when the user changes
}, [userId]);
// ...
}It works, but it's doing it the hard way — reset-on-change is exactly what React's key is for. Give the component a key that changes with the user & React throws away the old instance and mounts a fresh one, with all its state back at its initial value. No effect, no manual resetting of each field:
<Editor key={userId} userId={userId} />Changing the key is React's built-in "start over" button. When you find yourself writing an effect whose whole job is to reset state because some identity changed, a key almost always does it more cleanly — and it resets all the component's state, not just the one field you remembered.
You don't need an effect for something a user did
If code should run because the user clicked, typed or submitted, it belongs in the event handler — not in an effect watching for the state that the click changed.
// Roundabout: click sets state, effect reacts to the state.
function Cart() {
const [items, setItems] = useState([]);
useEffect(() => {
if (items.length > 0) {
showToast("Added to cart");
}
}, [items]);
function addItem(item) {
setItems([...items, item]);
}
}Now the toast fires whenever items changes for any reason — including things that aren't "the user added something," like loading a saved cart. The logic got detached from the thing that caused it. Put it back where it happened:
function addItem(item) {
setItems([...items, item]);
showToast("Added to cart"); // it happened because of this click
}Effects are for synchronizing with things outside React — the network, the DOM, a subscription, a timer. They are not for reacting to your own state changes. If you can name the exact user action that should trigger the code, that action's handler is the home for it.
The quick test
Before writing an effect, ask: "what event is this responding to?" If the answer is a user action (a click, a submit, a keypress), it goes in the handler. Only if the answer is "something outside React changed" — a response arrived, the tab regained focus, a socket pushed data — does it belong in an effect.
The effects that should stay
None of this means effects are bad. It means most of them are misfiled. The ones that genuinely belong are the ones synchronizing your component with the outside world:
- Subscribing to something and unsubscribing on cleanup — a WebSocket, an event listener, a store from outside React.
- Kicking off a fetch when the component mounts or an id changes (and libraries like React Query exist precisely because doing this well — caching, cancellation, race handling — is more than one effect's worth of work).
- Imperatively touching the DOM after render — focusing an input, measuring an element, driving a non-React widget.
Every one of these has the same shape: React needs to stay in step with something it doesn't control. That's the actual job of useEffect.
The cleanup nobody writes: race conditions in fetches
Since fetching is the one effect almost everyone keeps, here's the bug almost everyone leaves in it. You fetch on id change, id changes twice quickly & the responses come back out of order — the slow first request lands after the fast second one and overwrites it. Now you're showing the wrong user's data with the right id in the URL.
useEffect(() => {
let active = true;
fetchUser(id).then((user) => {
if (active) setUser(user); // ignore if a newer request started
});
return () => {
active = false; // this effect is stale now
};
}, [id]);The cleanup function marks the in-flight request as abandoned, so its result gets ignored. This is what effect cleanup is for — and skipping it is how you get the maddening kind of bug that only shows up when someone clicks fast.
The one habit that fixes most of this
Before you write useEffect, ask one question: is this synchronizing with something outside React? If yes, it's probably a real effect — write it & don't forget the cleanup. If no — if you're computing a value, reacting to a click, or resetting state — the effect is the wrong tool & there's a simpler answer that's also more correct.
Fewer effects isn't a stylistic preference. Each one you remove is an extra render gone, a stale-state window closed & a dependency array you no longer have to keep honest. The best useEffect is usually the one you realized you didn't need.