useCallback vs useMemo: When Each One Actually Helps
Both hooks memoize things — but memoizing the wrong thing is worse than not memoizing at all. Here's the mental model that makes it click.
Technologies Discussed
The Confusion
Developers often sprinkle `useCallback` and `useMemo` everywhere "for performance" — and then wonder why their app is *slower*. Memoization has overhead. Used wrong, it costs more than it saves.
Here's the clear model.
useCallback: Memoize a Function Reference
tsx
const handleSubmit = useCallback(() => {
submitForm(formData)
}, [formData])
`useCallback` returns the **same function object** across renders, as long as dependencies don't change.
**When it actually helps:** When you pass a callback to a child wrapped in `React.memo`. Without it, a new function is created on every render, and `React.memo`'s shallow comparison sees a "new" prop and re-renders anyway.
tsx
// Without useCallback — memo does nothing
const Parent = () => {
const onClick = () => doSomething() // new reference every render
return <MemoizedChild onClick={onClick} />
}// With useCallback — memo works as intended const Parent = () => { const onClick = useCallback(() => doSomething(), []) return <MemoizedChild onClick={onClick} /> }
useMemo: Memoize a Computed Value
tsx
const sortedList = useMemo(() => {
return items.sort((a, b) => a.name.localeCompare(b.name))
}, [items])
`useMemo` caches the **result of a computation** and only recomputes when dependencies change.
**When it actually helps:** Expensive calculations — filtering large datasets, complex transformations, or building derived state that would otherwise run on every keystroke.
The Litmus Test
Ask yourself two questions:
1. **Is this computation genuinely expensive?** (sorting 10,000 items = yes; mapping 5 items = no) 2. **Does this value/function flow into a memoized child?**
If the answer to *both* is no — skip the hook. You're adding complexity and overhead for no gain.
Common Mistake
tsx
// 🚫 Pointless — no memoized child, no expensive computation
const label = useMemo(() => `Hello, ${name}`, [name])// ✅ Just write it const label = `Hello, ${name}`
Quick Reference
| Hook | Memoizes | Use when | |---|---|---| | `useCallback` | Function reference | Passing to a `React.memo` child | | `useMemo` | Computed value | Expensive calculation, or expensive object identity |
Takeaway
Don't memoize by default. Profile first, optimize second. When you *do* reach for these hooks, the rule is simple: **useCallback for functions, useMemo for values** — and only when there's a measurable reason to.