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.

