The problem these hooks solve
Every render of a React component runs the function body from top to bottom. Every function and object defined inside that body is a brand new reference each time.
function Parent() {
const options = { color: 'blue' } // new object every render
const handleClick = () => save() // new function every render
return <Child options={options} onClick={handleClick} />
}
Most of the time this is fine — React's reconciler is fast, and a new reference doesn't
cause visible slowness. But it breaks two specific optimization mechanisms: React.memo
and effect dependency arrays. useCallback and useMemo solve exactly this by giving
you stable references that only change when their dependencies change.
useCallback
useCallback(fn, deps) returns a memoized function. On each render it compares deps
with the previous render's deps using Object.is. If nothing changed, it returns the
exact same function reference from last time.
const handleSort = useCallback((column) => {
setSort(column)
}, []) // deps: empty — sort setter is always the same
This is purely about referential stability — the function still runs normally when
called. The optimization is that handleSort === previousHandleSort between renders
where the deps didn't change.
useMemo
useMemo(() => computation, deps) caches the return value of a computation. It only
re-runs the computation when a dependency changes.
const filtered = useMemo(
() => products.filter(p => p.active).sort((a, b) => a.price - b.price),
[products]
)
Without useMemo, the filter and sort run on every render — even on a keystroke that
updates completely unrelated state. With it, the sorted result is reused until products
changes.
The relationship between the two
useCallback(fn, deps) is exactly useMemo(() => fn, deps). They're the same mechanism:
one is specialized for the "the value is a function" case.
const fn = useCallback(() => compute(x), [x])
// identical to:
const fn = useMemo(() => () => compute(x), [x])
In practice, prefer the purpose-fit name: useCallback for stable function references,
useMemo for stable computed values.
Why referential equality matters
JavaScript compares objects and functions by reference (memory address), not by content. Two identically written functions are never equal to each other:
(() => {}) === (() => {}) // false — different objects in memory
const fn = () => {}
fn === fn // true — same reference
This matters because React.memo bails out of a re-render only when every prop is
Object.is-equal to the previous render. An inline function prop fails that check on
every render, defeating the memo.
Combining React.memo with useCallback
The pattern that actually works: React.memo on the child component, useCallback for
the function props passed from the parent.
// Child: skip re-renders when props are equal
const Row = React.memo(function Row({ item, onDelete }) {
return <div>{item.name} <button onClick={() => onDelete(item.id)}>x</button></div>
})
// Parent: stable function reference so memo can work
function List({ items }) {
const handleDelete = useCallback(id => {
setItems(prev => prev.filter(i => i.id !== id))
}, [])
return items.map(item => <Row key={item.id} item={item} onDelete={handleDelete} />)
}
Without useCallback, handleDelete is a new function each render → Row's memo
never hits → every item re-renders on any list change.
Stabilizing effect dependencies
Functions defined inside a component are new references every render. If an effect
depends on such a function, it re-runs on every render — the very problem useEffect
deps are supposed to prevent.
// without useCallback: refetch fires every render
function Page({ userId }) {
const fetchUser = () => api.get(userId)
useEffect(() => { fetchUser().then(setUser) }, [fetchUser]) // new fn each render
}
// with useCallback: refetch fires only when userId changes
const fetchUser = useCallback(() => api.get(userId), [userId])
useEffect(() => { fetchUser().then(setUser) }, [fetchUser])
An alternative that's often cleaner: move the function inside the effect so it's not in the dependency array at all.
Stabilizing object props with useMemo
The same referential-equality issue applies to objects and arrays. Wrap them in useMemo
to preserve the reference when the content hasn't changed.
// new object every render -> memoized child always re-renders
<Chart config={{ color: theme, size }} />
// stable reference -> child re-renders only when theme or size change
const config = useMemo(() => ({ color: theme, size }), [theme, size])
<Chart config={config} />
For context providers, memoizing the value object is the fix for "all consumers re-render on every Provider render."
When NOT to memoize
This is what most developers miss: useCallback and useMemo have real costs.
Every render, React must:
- Read the stored deps array.
- Compare each dep with
Object.is. - Return the cached value or re-run the computation.
For trivial computations, this overhead exceeds the cost of just re-running the computation. The React team has publicly said that the overhead of memoization can make performance worse for simple cases.
// overhead > savings
const label = useMemo(() => `Hello ${name}`, [name])
// just compute it
const label = `Hello ${name}`
Memoize when you have evidence of a problem (profiling with React DevTools Profiler), not as a default practice. The three cases worth memoizing:
- Genuinely expensive computation — filtering/sorting thousands of items.
- Stable reference for a memoized child —
React.memocomponent receiving object or function props. - Effect dependency stabilization — function or object in a
useEffectdep array.
Stale closure: the missing dependency bug
Omitting a value from the deps array means the memoized function/value closes over a stale snapshot of that value from the render where it was last computed.
// user is omitted -> greeting always uses the initial user.name
const greeting = useMemo(() => `Hi ${user.name}`, []) // bug: stale forever
// correct
const greeting = useMemo(() => `Hi ${user.name}`, [user.name])
The react-hooks/exhaustive-deps ESLint rule catches this. Never suppress the rule to
"cache something forever" — if you genuinely want a value from a specific moment in
time, store it in a useRef.
Measuring before optimizing
The correct workflow:
- Notice a perceived performance issue.
- Open React DevTools Profiler and record the interaction.
- Identify which component is the hot path and why.
- Apply
useMemo/useCallback/React.memosurgically. - Record again to confirm the improvement.
Wrapping everything in useCallback "just in case" adds code, slows down renders for
most code paths, and makes dependency arrays harder to maintain.
Common interview questions at a glance
- What does useCallback return? A memoized function reference, identical across renders until a dependency changes.
- What does useMemo return? A memoized computed value, recomputed only when deps change.
- How do they differ? Same mechanism;
useCallbackcaches a function,useMemocaches any value (often the result of calling a function). - Why doesn't React.memo work if I don't useCallback? Function props are new references each render; memo sees them as changed.
- When should you memoize? When profiling shows a real problem — not by default.
- What's the stale closure risk? Omitting a dep means the memoized value reads a frozen old snapshot of that dep forever.