Why I Stopped Reaching for useEffect (And You Probably Should Too)

Most useEffect calls in modern React are mistakes. Here's the mental model I use now — when to skip it, what to replace it with, and the one question that catches half of them.

2 min read

For a long time, my React muscle memory was simple: see something happen on the screen, add a useEffect. Page mounts? useEffect. Props change? useEffect. Need to compute something? You guessed it.

Then I sat down and read the React team's You Might Not Need an Effect guide, slowly, and realized about half my effects were doing nothing useful. Some were actively making the app slower or buggier.

Here's the mental model I use now.

The three things useEffect is usually wrong for

Derived state

Any time your "effect" is just computing one piece of state from another, you don't need an effect at all.

// Bad
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
 
// Good
const fullName = `${firstName} ${lastName}`;

The effect version triggers an extra render. The plain version doesn't. Same output, half the work.

Transforming props

If you're using an effect to keep local state in sync with a prop, the prop already is the state. Just use it.

// Bad
const [items, setItems] = useState([]);
useEffect(() => {
  setItems(props.items.filter((i) => i.active));
}, [props.items]);
 
// Good
const items = useMemo(() => props.items.filter((i) => i.active), [props.items]);

You only need useMemo if the computation is genuinely expensive. For most filters and maps, the inline version is fine.

Resetting state when something changes

The classic: a profile page that takes a userId prop, and you want to clear local form state when the user changes.

// Tempting but wrong
useEffect(() => {
  setDraft("");
}, [userId]);
 
// Better — let React remount the component
<ProfileEditor key={userId} userId={userId} />;

Re-mounting via key gets you a fresh state slot for free. No effect, no manual cleanup, no race conditions.

When useEffect is actually right

Effects are for synchronizing with systems React doesn't own. That's it.

  • Subscribing to a WebSocket or an event listener
  • Sending analytics on route changes
  • Reading from localStorage once on mount
  • Focusing a DOM element imperatively after render

If your effect's body doesn't touch the outside world, you're probably doing something React can already do for you during render.

The one-question filter

Every time I'm about to write a useEffect, I now ask:

What would happen if I deleted this and computed it during render instead?

If the answer is "nothing breaks," I delete it.

If the answer is "I'd lose a subscription, a DOM call, or some persistent thing," I keep it.

That single check has caught maybe 40% of the effects I would've written a year ago. The result is fewer renders, fewer subtle bugs, and code that's easier to follow when I come back to it three months later.

If you've been writing React for a while and never read the effects guide end to end, do yourself a favor and spend twenty minutes with it. It's the single highest-leverage piece of React documentation that exists, and most of us learned the library before it was written.

Related posts