useMemo vs useCallback
`useMemo(fn, deps)` caches the *return value* of `fn`. `useCallback(fn, deps)` caches the *function itself*. `useCallback(fn, d)` is exactly `useMemo(() => fn, d)`. Use them for referential stability of values/functions passed to memoized children, and for genuinely expensive computations. In React 19+, the React Compiler handles most of these automatically.
Same machinery, different return value.
useMemo(() => compute(a, b), [a, b]); // caches the result of compute()
useCallback((x) => fn(a, x), [a]); // caches the function itselfuseCallback(fn, d) is literally useMemo(() => fn, d). Two names so the intent is obvious at the call site.
Two reasons to use them.
1. Referential stability for memoized children.
React.memo skips re-rendering if props are shallow-equal. A new object/function each render breaks the bailout:
// Bad — Row.memo can never skip; new onClick every render
items.map(item => (
<Row item={item} onClick={() => select(item.id)} />
))
// Good — stable handler
const onClick = useCallback((id) => select(id), [select]);
items.map(item => (
<Row item={item} onClick={onClick} />
))Same with values:
const config = useMemo(() => ({ theme, dense }), [theme, dense]);
<Header config={config} />2. Genuinely expensive computations.
const sorted = useMemo(() => heavySort(items), [items]);If items doesn't change, you skip the sort. Without useMemo, the sort runs every render — even when a sibling state triggered the render, not items.
Common misuse.
- Wrapping cheap computations in
useMemo. The hook itself has overhead (creating the deps array, comparing it). For a one-line transform, it's a wash or a loss. - Wrapping callbacks in
useCallbackwhen nothing memoized consumes them. - Passing primitives —
useMemo(() => 42, [])is silly; primitives are already referentially equal. - Trusting memoization to fix a state-in-the-wrong-place problem. Lift state down instead.
The dep-array footgun.
ESLint's react-hooks/exhaustive-deps rule will tell you. Listen to it. Disabling it because "I know what I'm doing" is the source of 80% of stale-closure bugs in production:
// Bug — onClick captures the initial value of count forever
const onClick = useCallback(() => {
console.log(count); // stale
}, []); // missing dep: countThe React Compiler changes the rules (React 19+).
In compiler-on codebases, you mostly don't write these hooks anymore. The compiler analyses your component, infers stability needs, and auto-memoizes. Manual hooks are reserved for:
- Genuinely heavy computations the compiler can't see is heavy.
- Cases outside the compiler's analysis scope (interop with non-compiled code).
The "memoize every callback to be safe" reflex is now actively unhelpful — it adds noise and can interact badly with the compiler.
Senior cheat sheet.
| Goal | Pre-compiler | With compiler |
|---|---|---|
| Stable callback for memoized child | useCallback | nothing |
| Expensive compute | useMemo | useMemo (still useful) |
| Stable object/array prop | useMemo | nothing |
| Reset subtree on key change | <C key={...} /> | <C key={...} /> |
Quick disambiguation in interviews. "useMemo memoizes values, useCallback memoizes functions, useCallback is shorthand for useMemo wrapping a function literal." If they ask for use cases, name the memoized child / referential equality pattern first — that's the right answer for ~80% of real usage.
Follow-up questions
- •When is `useMemo` cheaper than just recomputing?
- •Why doesn't `useCallback` alone speed anything up?
- •What does the React Compiler change about manual memoization?
- •How would you debug a stale-closure bug from missing deps?
Common mistakes
- •Wrapping every function in useCallback even when no memoized child consumes it.
- •Disabling the `exhaustive-deps` lint rule to suppress warnings.
- •Memoizing primitives.
- •Believing useMemo *guarantees* the cached value will persist — React may discard caches under memory pressure.
Performance considerations
- •Hook overhead is small but non-zero — not free, especially in lists.
- •Profile before adding either; most components don't need them.
Edge cases
- •React may drop memoized values under memory pressure — don't rely on identity for correctness.
- •Strict Mode runs hooks twice; useMemo callbacks must be pure (no side effects).
Real-world examples
- •Lists with thousands of memoized rows — useCallback for shared handlers.
- •Charts deriving from raw data — useMemo for the transform.