53

Stop using useEffect for Everything

Senior Developer
  • Twitter
  • LinkedIn

For many React developers, (admittedly myself included) useEffect is the first hook they truly learn and unfortunately, the last one they ever reach for.

Need derived state? useEffect. Sync external data? useEffect. Manage complex state transitions? useEffect.

It works… until it doesn’t. Over time, apps become harder to reason about, harder to optimize, and harder to debug.

React gives us better primitives, and although they are a little more nuanced and harder to grasp, they should be used far more.

 

The Problem with Overusing useEffect

useEffect is designed for side effects:

  • Mutating things outside React
  • Running code after render

But it’s often misused for:

  • Deriving state from props
  • Synchronizing values
  • Managing business logic
  • Orchestrating state machines

This leads to:

  • Double renders
  • Dependency array bugs
  • Infinite loops
  • Logic that runs after paint when it should run during render

If your effect exists only to “keep state in sync,” it’s probably the wrong tool.

 

useMemo: Derived State Without Side Effects

If a value can be calculated from props or state, it does not belong in useEffect.

The common mistake

const [total, setTotal] = useState(0);

 

useEffect(() => {

 setTotal(items.reduce((sum, i) => sum + i.price, 0));

}, [items]);

This introduces:

  • An extra render
  • State that can get out of sync
  • Unnecessary complexity

The better approach

const total = useMemo(

  () => items.reduce((sum, i) => sum + i.price, 0),

 [items]

);

Why this is better

  • No extra state
  • No side effects
  • No render lag
  • Easier to reason about

If you’re using useEffect to compute something, stop and reach for useMemo instead.

 

useReducer: State Transitions, Not State Reactions

When state updates depend on previous state or complex rules, useReducer is almost always a better choice than chaining effects.

The useEffect trap

useEffect(() => {

  if (status === "loading") {

   setButtonDisabled(true);

  } else {

   setButtonDisabled(false);

  }

}, [status]);

Now logic is split:

  • State change happens in one place
  • Reaction happens somewhere else

With useReducer

function reducer(state, action) {

 switch (action.type) {

   case "LOAD":

     return { ...state, status: "loading", disabled: true };

   case "SUCCESS":

     return { ...state, status: "success", disabled: false };

  }

}

Why this scales better

  • State transitions are explicit
  • Logic is centralized
  • No reactive side effects
  • Easier to test and debug

useReducer models intent. useEffect models reaction. Most UI logic is intent-driven.

 

useSyncExternalStore: The Right Way to Sync External State

Many developers still use useEffect to subscribe to external data sources:

 

useEffect(() => {

 store.subscribe(setValue);

 return () => store.unsubscribe(setValue);

}, []);

This pattern:

  • Breaks concurrent rendering guarantees
  • Causes tearing
  • Is unsafe for modern React

The modern solution

 

const value = useSyncExternalStore(

 store.subscribe,

 store.getSnapshot

);

Why this matters

  • Built specifically for external stores
  • Works correctly with concurrent rendering
  • No manual effects
  • No race conditions

If you’re integrating:

  • Global state stores
  • CMS event systems
  • Browser APIs
  • Custom data sources

useSyncExternalStore is the correct abstraction.

 

Mental Model Shift: Render First, Effects Last

A useful rule of thumb:

  • useMemo → “Can this be derived during render?”
  • useReducer → “Is this a state transition?”
  • useSyncExternalStore → “Is this external to React?”
  • useEffect → “Does this have to run after render?”

If the answer to the last question is “not really,” don’t use useEffect.

 

Why This Matters for Performance and Stability

Overusing useEffect:

  • Delays logic until after paint
  • Adds unnecessary renders
  • Hurts INP and perceived performance
  • Makes bugs harder to trace

Using the right hook:

  • Reduces renders
  • Improves predictability
  • Makes code self-documenting
  • Plays nicely with modern React features

 

Final Thought

useEffect isn’t bad, its just overused.

React has evolved. The hooks ecosystem now reflects clear separation of concerns:

  • Computation
  • State transitions
  • External synchronization
  • Side effects

When you stop reaching for useEffect by default and start choosing the right tool, your components become faster, clearer, and easier to maintain.

And your future self will thank you.