useMemo returns a memoized value, computed by a function you provide, recalculated only when one of the listed dependencies changes. useCallback returns a memoized function reference itself, without invoking it, also recalculated only when a dependency changes. Both hooks solve the same underlying problem — avoiding unnecessary work caused by new references being created on every render — but they operate on different kinds of output, and mixing up when each one applies leads to a fair amount of the confusion around them.
The rest of this post works through the common misconceptions directly, pairing each one with what the behavior actually is.
Myth: useCallback Is Just useMemo for Functions, So They’re Interchangeable
There’s a kernel of truth here, which is probably why the myth persists. In fact, useCallback(fn, deps) is functionally equivalent to useMemo(() => fn, deps) — the React source code essentially implements it that way. But equivalence at the implementation level doesn’t mean the two are interchangeable in practice.
Reality: useMemo executes the function you pass it during render and caches the result of that execution. useCallback does not execute anything — it caches the function itself, unexecuted, so it can be passed down or called later.
// useMemo: caches the RESULT of calling the function
const sortedList = useMemo(() => {
return items.slice().sort((a, b) => a.value - b.value);
}, [items]);
// useCallback: caches the FUNCTION, does not call it
const handleSort = useCallback(() => {
sortItems(items);
}, [items]);
Trying to use useMemo to memoize a function without wrapping it in an extra arrow function is technically possible but backwards for the intent — you’d be computing a function as a value, which is exactly what useCallback already expresses more clearly. Reach for useCallback when the thing you need to stay stable across renders is a callback that gets invoked somewhere else, typically inside useEffect dependencies or as a prop passed to a memoized child.
Myth: Both Hooks Prevent Re-Renders
This is probably the most common misunderstanding, and it causes more wasted useMemo/useCallback calls than any other single misconception.
Reality: Neither hook, on its own, prevents anything from re-rendering. useMemo skips recalculating a value; useCallback skips recreating a function reference. Neither one wraps a component in a rendering guard. A component wrapped in neither hook re-renders exactly as often with or without them present in its parent — what changes is whether the props being passed down are referentially stable.
The re-render prevention only happens when these stable references are combined with React.memo on the receiving component:
const Child = React.memo(function Child({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click me</button>;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, this is a new function reference on every
// Parent render, so React.memo on Child provides no benefit at all.
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
</>
);
}
Remove React.memo from Child in that example, and useCallback accomplishes nothing observable — Child re-renders on every Parent update regardless of whether handleClick’s reference is stable. The two hooks are a pair with React.memo, not a standalone rendering optimization.
Myth: More Memoization Is Always Safer
Wrapping every value and every function in these hooks feels like a defensive habit — it can’t hurt, the reasoning goes, so why not apply it everywhere?
Reality: Both hooks carry cost. useMemo has to store the previous dependency array, compare it against the new one on every render, and hold onto the cached value in memory. For a computation like items.length > 0, that comparison overhead is larger than simply recalculating the value would have been. The same applies to useCallback wrapping a function that gets created and discarded cheaply regardless.
A short list of situations where the overhead outweighs the benefit:
- Simple arithmetic or string operations that take microseconds to compute directly
- Functions passed only to native DOM elements (
<button onClick={...}>) with noReact.memoanywhere downstream - Values recalculated on every render anyway because their dependency array changes just as often as the component itself renders
None of this means memoization is a mistake — it means the decision needs a reason attached to it, usually one confirmed by profiling data rather than a general sense that memoizing more is inherently protective.
Myth: The Dependency Arrays Work the Same Way for Both
Since both hooks accept a dependency array with identical syntax, it’s easy to assume the mental model for populating that array is the same in each case.
Reality: The syntax matches, and the exhaustive-deps ESLint rule applies to both identically — but what’s typically inside those functions differs in a way that affects how often bugs show up. useCallback’s function body frequently closes over props, state, or other functions, so a missing dependency shows up as an unexpectedly stale closure: the function fires with an old value baked in, calling setCount on a value from three renders ago rather than the current one.
useMemo’s function body is often a pure calculation over its inputs — filtering an array, formatting a number, deriving a total — which makes a missing dependency more likely to produce a stale value rather than a stale side effect. Both are bugs worth catching, but they tend to surface differently: a stale callback shows up as behavior that seems to lag one interaction behind, while a stale memoized value shows up as UI that doesn’t match the underlying data until something else forces a re-render.
// Stale closure bug: missing `count` in the dependency array
const handleIncrement = useCallback(() => {
setCount(count + 1); // always adds 1 to whatever `count` was at mount
}, []); // should include `count`, or use the functional update form
The fix in cases like this is often to use the functional update form (setCount(c => c + 1)) rather than adding more dependencies, since it sidesteps the staleness problem entirely by not closing over count at all.
Side-by-Side Summary
| Question | useMemo | useCallback |
|---|---|---|
| What does it return? | A memoized value (the result of calling a function) | A memoized function reference (unexecuted) |
| Does it prevent re-renders by itself? | No | No |
| When does it matter for rendering? | When the value is a prop passed to a React.memo component, or used in another hook’s dependency array |
When the function is a prop passed to a React.memo component, or used in another hook’s dependency array |
| Typical bug from a missing dependency | Stale derived value | Stale closure over state or props |
| Common overuse case | Memoizing cheap calculations | Memoizing functions with no memoized consumer downstream |
Where This Leaves the Decision
Choosing between the two isn’t really a choice at all once the underlying question is framed correctly: are you trying to stabilize a computed value, or a function reference? The answer to that question picks the hook. The harder question — whether either one is worth adding at that point in the code — depends on whether something downstream, usually a React.memo-wrapped component or a useEffect dependency array, is actually going to benefit from the stability being provided.
Before adding either hook to a new piece of code, it’s worth asking what consumes the value or function once it leaves this component. If nothing downstream cares whether the reference is stable between renders, the memoization is solving a problem that doesn’t exist yet.
🔗 Recommended Reading
- Automated Performance Regression Testing for React: A Practical Setup Guide
- React Hydration Performance: A Step-by-Step Guide to Diagnosing and Fixing Slow Hydration
- Real User Monitoring for React Performance: A Production Case Study
- React Fiber Architecture Explained: Why It Matters for Performance
- Redux, Zustand, or Jotai: A Troubleshooting Guide to Global State Performance