React useEffect vs useMemo vs useCallback — When To Use What
If you’ve ever asked yourself:
- When should I use
useEffect? - What’s the actual difference between
useMemoanduseCallback? - Am I over-optimizing my React app?
You’re not alone.
These three hooks are some of the most misunderstood parts of React. They’re often overused, misused, or added “just in case” — which can actually hurt performance instead of helping it.
Abstract
In this article, we’ll break down:
- what each hook is responsible for
- when to use each hook
- when not to use them
- how they work together in a real-world example
No magic. No memorization.
Just mental models that actually stick.
Table of Contents
- The Problem: Why This Gets Confusing
useEffect— For Side Effects (Not Logic)useMemo— For Caching Expensive ValuesuseCallback— For Stable Function References- The Memory Trick (This One Actually Works)
- A Real-World Example Using All Three
- A More Realistic Pattern
- Final Takeaway
The Problem: Why This Gets Confusing
Here’s a common situation:
- Your component re-renders constantly
- Expensive calculations run on every render
- Child components re-render even when nothing changed
- API calls or side effects fire more often than expected
Most of the time, these issues come from:
- using
useEffectfor the wrong reasons - overusing
useMemo - using
useCallbackwithout understanding function identity
useEffect — For Side Effects (Not Logic)
What useEffect Does
useEffect runs after your component renders.
Its only job is to handle side effects — things that interact with the world outside React’s render cycle.
useEffect(() => {
// side effect
}, [dependencies]);
useMemo — For Caching Expensive Values
useMemo memoizes a value and recomputes it only when dependencies change.
const value = useMemo(() => expensiveFn(input), [input]);
useCallback — For Stable Function References
useCallback memoizes a function reference so it doesn’t change between renders.
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
The Memory Trick (This One Actually Works)
useEffect→ runs side effectsuseMemo→ stores valuesuseCallback→ stores functions
A Real-World Example Using All Three
function App() {
const [count, setCount] = useState(0);
const value = useMemo(() => slowFn(count), [count]);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
useEffect(() => {
console.log(count);
}, [count]);
return <Child value={value} onClick={handleClick} />;
}
Each hook has one clear responsibility — and they don’t overlap.
A More Realistic Pattern
function App() {
const [count, setCount] = useState(0);
const expensiveValue = useMemo(
() => slowFunction(count),
[count]
);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
useEffect(() => {
console.log("Count changed:", count);
}, [count]);
return (
<div>
<Display value={expensiveValue} />
<Button onClick={handleClick} />
</div>
);
}
const Button = React.memo(function Button({ onClick }) {
return <button onClick={onClick}>Increment</button>;
});
Now the optimizations actually matter.
Final Takeaway
- run code after render →
useEffect - cache expensive values →
useMemo - keep function references stable →
useCallback
Don’t optimize blindly. Optimize with intention.