JGJonathan Gobielweb platform engineer
Back

stop overusing useEffect react

Stop Overusing useEffect in React: Keep Effects for Synchronization, Not Routine Component Logic

3 min readSource

When a React component gets messy, the root cause is often not state volume.

It is effect sprawl.

We keep dropping logic into useEffect because it feels safe, then wonder why behavior becomes hard to reason about.

TL;DR

  • Use useEffect for synchronization with external systems.
  • Keep derivations in render or useMemo when expensive.
  • Keep event-driven actions in handlers.
  • Fewer effects usually means fewer weird bugs.

What Problem Does This Solve

Overusing useEffect creates three failure modes:

  • hidden control flow split across multiple hooks,
  • dependency arrays that are technically correct but semantically fragile,
  • avoidable re-renders from derived-state updates.

This causes unpredictable behavior and maintainability drift, exactly as your reference points out.

When useEffect Is Actually the Right Tool

Use useEffect when React needs to sync with something outside render:

  • data fetching tied to lifecycle,
  • event listener subscription and cleanup,
  • timers and intervals,
  • imperative integrations.

If no external system is involved, effect usage is often a smell.

Common Anti-Patterns and Refactors

1. Derived state inside an effect

Anti-pattern:

const [visibleUsers, setVisibleUsers] = useState(users)

useEffect(() => {
  setVisibleUsers(users.filter((u) => u.active))
}, [users])

Refactor:

const visibleUsers = users.filter((u) => u.active)

The refactor removes one state variable and one render loop.

2. Event reaction in effect instead of handler

Anti-pattern:

useEffect(() => {
  if (submitted) {
    sendAnalytics("submit")
  }
}, [submitted])

Refactor:

function onSubmit() {
  sendAnalytics("submit")
  setSubmitted(true)
}

Place side effects where intent is initiated.

3. Valid synchronization effect

useEffect(() => {
  const handler = () => setWidth(window.innerWidth)
  window.addEventListener("resize", handler)

  return () => window.removeEventListener("resize", handler)
}, [])

This is valid because it syncs component state with browser events and includes cleanup.

useMemo and useCallback: Use With Intent

Your reference mentions useMemo and useCallback as alternatives.

That is correct, with one caveat.

Do not add them by default.

Use them when you have either:

  • measurable expensive computation,
  • referential stability requirements for child memoization.

Otherwise, plain render logic is usually clearer.

Trade-Offs and Limitations

Removing effects aggressively can backfire if you misunderstand lifecycle responsibilities.

Two common mistakes:

  • forgetting cancellation or cleanup for async work,
  • moving true side effects into render path.

The goal is not zero effects.

The goal is correct effects.

Practical Cleanup Workflow

  1. List every effect in a component.
  2. Label each one: sync, derivation, or event logic.
  3. Remove derivation effects first.
  4. Move event logic to handlers.
  5. Keep sync effects and verify cleanup behavior.

This process usually shrinks component complexity fast.

FAQ

Should I avoid useEffect entirely?

No.

You should avoid unnecessary useEffect usage.

Legitimate synchronization effects are still essential.

How do I know an effect is unnecessary?

If it does not synchronize with something external and only derives local values, it is likely unnecessary.

Does fewer effects always improve performance?

Often, but not automatically.

The bigger win is predictability and readability, which then makes performance tuning easier.

What should I profile first?

Start with components that re-render frequently and effects that trigger state updates.

Those areas tend to hide the largest avoidable cost.

Closing

React gets easier when we stop treating useEffect as a default bucket for logic.

Audit one complex component this week, remove one unnecessary effect, and compare how quickly your team can reason about the code afterward.