Stop Drowning Your Code in useEffect

useEffect Problem

React is built on a simple promise: UI = f(state). Your component should be a pure function of the data you give it. That’s the magic. You declare what you want, and React figures out how to keep the DOM in sync.

And then there’s useEffect.

Somewhere along the way, useEffect became the duct tape of React development. Data fetching? Throw it in an effect. Syncing props to state? Effect. Formatting a value? Effect. Accidentally re-rendering too much? Just tweak the dependency array until it stops yelling at you.

This isn’t React anymore. It’s imperative spaghetti wrapped in hooks.


What useEffect Is Actually For

At its core, useEffect is about side effects: things that live outside the React rendering model.

  • Subscribing to a WebSocket.
  • Registering a global event listener.
  • Starting or cleaning up a timer.

That’s it. Effects are escape hatches, not primary tools. They’re where you deal with the messy, stateful, non-React world.


What Developers Use It For Instead

But what do we see in codebases everywhere?

  • State synchronization hacks

    useEffect(() => {
      setSomething(props.something)
    }, [props.something])
    

    That’s not a side effect. That’s duplicating props into state.

  • Derived values in disguise

    useEffect(() => {
      setFullName(`${first} ${last}`)
    }, [first, last])
    

    This should just be a computed value:

    const fullName = `${first} ${last}`
    
  • Over-engineered fetching
    Wrapping every async call in an effect, even when data could be handled with a loader, a server component, or a query library.

Each of these erodes React’s declarative model. They turn a clean “UI = f(state)” into “UI = some state I hope I synced correctly in an effect somewhere.”


Why It’s a Problem

  1. Hidden complexity: Every effect adds another async subscription, another cleanup to remember.
  2. Race conditions galore: Stale closures, overlapping fetches, dependency array games.
  3. Performance debt: Unnecessary renders and re-renders, wasted cycles doing what React already does for free.
  4. Mental overhead: Instead of trusting React’s model, you end up second-guessing every dependency and chasing phantom bugs.

The result: more code, less clarity.


The Alternative: Trust the Model

Most useEffect code is just React developers not trusting React.

  • If you need a derived value → compute it inline or memoize it.
  • If you need to fetch data → use a data-fetching abstraction (React Query, SWR, RSC loaders).
  • If you need to sync state → question why you’re duplicating it in the first place.

Save useEffect for the things React cannot do declaratively. Treat it like a sharp knife: sometimes essential, but you don’t cut every sandwich with a chainsaw.


The Rant in a Sentence

useEffect isn’t bad. But overusing it is a code smell that tells me you’re fighting React instead of using it. If your component logic lives in a forest of effects, you don’t have a React app,you’ve reinvented jQuery with extra steps.