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

 

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);
}, []);

The modern solution:


const value = useSyncExternalStore(
  store.subscribe,
  store.getSnapshot
);

 

Mental Model Shift: Render First, Effects Last


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?”

 

Final Thought

useEffect isn’t bad — it’s 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.