4 min read
When to Use useMemo and useCallback
There is a common pattern I see in React codebases: every computed value wrapped in useMemo, every callback wrapped in useCallback, as if memoisation were always free. It isn't. Both hooks add overhead, and applying them without measuring first often makes things slower, not faster.
Here is the honest version: useMemo and useCallback are tools for specific problems. They are not a general performance strategy.
What They Actually Do
useMemo caches the result of a computation between renders. React stores the return value and only recomputes it when a listed dependency changes.
useCallback does the same thing for functions. It returns the same function reference between renders unless a dependency changes.
const filtered = useMemo(() => {
return items.filter((item) => item.active)
}, [items])
const handleSubmit = useCallback((data: FormData) => {
onSubmit(data)
}, [onSubmit])
Both hooks still run on every render. They compare the dependency list, and if nothing has changed, they hand back the cached value. That comparison is not free.
When useMemo Helps
The case for useMemo is genuinely expensive computations. If you have a list of thousands of items being sorted, filtered, and formatted on every render, and the source data rarely changes, memoising that work makes sense. The cost of the computation is high enough that avoiding it is worth the overhead of the cache check.
const sortedAndFiltered = useMemo(() => {
return products
.filter((p) => p.category === selectedCategory)
.sort((a, b) => a.price - b.price)
.map((p) => ({ ...p, formattedPrice: `$${p.price.toFixed(2)}` }))
}, [products, selectedCategory])
The case against useMemo is simple computations. Wrapping items.length > 0 or a string concatenation in useMemo adds the dependency comparison overhead on top of work that was already trivial.
When useCallback Helps
useCallback is most useful when you pass a function down to a child component that is wrapped in React.memo. Without stable references, the child re-renders every time the parent renders, because a new function is created and the prop comparison fails.
// Without useCallback, ChildComponent re-renders every time Parent renders,
// because handleClick is a new function reference on every render.
const Parent = () => {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
setCount((c) => c + 1)
}, [])
return <ChildComponent onClick={handleClick} count={count} />
}
const ChildComponent = React.memo(({ onClick, count }: { onClick: () => void; count: number }) => {
return <button onClick={onClick}>{count}</button>
})
If ChildComponent is not wrapped in React.memo, using useCallback here does nothing useful. The child will re-render anyway, and you have added the hook overhead for no gain.
The Pattern That Does Not Help
The most common misuse I see is wrapping callbacks that are already stable or cheap, purely as a habit:
// This does nothing useful. The function is simple,
// and no memo boundary depends on it.
const handleToggle = useCallback(() => {
setOpen((prev) => !prev)
}, [])
This is not harmful in isolation, but it adds noise to the codebase and trains readers to trust useCallback as a signal that there is a real referential stability concern. When it is everywhere, that signal disappears.
How to Measure Before You Commit
The React DevTools Profiler is the right place to start. Record a user interaction, look at which components re-render and how long they take. If a component is not in the flame graph, it is not a problem.
If you do find a slow render, check whether the bottleneck is in the component itself or in child re-renders. That tells you whether you need useMemo for computation or useCallback plus React.memo for referential stability.
A rough heuristic before reaching for either hook:
- Is the computation taking more than a few milliseconds in the profiler?
- Is a child component re-rendering when its props have not semantically changed?
- Would removing the hook cause a measurable regression?
If the answer to all three is yes, the hook is probably justified. If you are just guessing, you are probably making the code harder to read for no measurable benefit.
The Real Trade-off
Memoisation trades memory and complexity for avoided computation. That trade only pays off when the avoided computation is more expensive than the overhead of caching and comparing dependencies. For most UI code, it isn't.
Start without useMemo and useCallback. Add them when a profiler tells you a specific component or computation is slow. The code will be easier to read, and you will have actual data to justify the added complexity.