A developer I was reviewing code with had used useCallback to memoize a computed value (not a function), and useMemo to memoize an event handler (a function), both technically working but reflecting a genuine misunderstanding of what each hook is actually designed for, which became apparent once we discussed why each existed as a separate hook rather than a single unified API.


The Core Distinction

useMemo memoizes a computed value — the result of a calculation. You pass a function that returns the value you want, and useMemo caches that returned value, recalculating only when its dependencies change.

useCallback memoizes a function reference itself — not what the function returns, but the function object. This matters specifically because functions are compared by reference in JavaScript, meaning a new function defined on every render is a genuinely different reference each time, even if its logic is identical.

// useMemo: caches the RESULT of expensive calculation
const sortedList = useMemo(() => expensiveSort(items), [items]);

// useCallback: caches the FUNCTION REFERENCE itself
const handleClick = useCallback(() => doSomething(items), [items]);

Why This Distinction Actually Matters in Practice

This connects directly to the prop reference stability issue covered in our React.memo guide. If you pass a function as a prop to a child component wrapped in React.memo, and that function is redefined on every parent render (without useCallback), the child’s memoization is defeated, since the function reference changes every time even though the underlying logic is identical.

useMemo would not actually solve this problem correctly if misapplied here, since wrapping a function definition in useMemo would memoize the function’s return value (if you called it), not provide a stable reference to the function itself in the way useCallback specifically does.

// Incorrect: this memoizes calling the function immediately, not the function itself
const handleClick = useMemo(() => () => doSomething(items), [items]);

// Correct: useCallback directly memoizes the function reference
const handleClick = useCallback(() => doSomething(items), [items]);

Both can technically be written to achieve a similar end result through workarounds, but useCallback exists specifically because this function-reference-stability pattern is common enough to warrant its own dedicated, clearer API.


When useMemo Genuinely Provides Measurable Benefit

I profiled a component performing a moderately expensive sort and filter operation on a list of several thousand items, recalculating on every render regardless of whether the underlying data had actually changed. Wrapping this calculation in useMemo with the correct dependency array reduced the actual measured time spent in this calculation across renders where the dependency had not changed, confirmed directly through React DevTools Profiler’s flame graph showing this specific calculation’s time before and after the change.

For genuinely cheap calculations — simple arithmetic, basic string formatting — useMemo provides no measurable benefit and adds the same kind of comparison overhead discussed in our React.memo guide, since there is no significant calculation cost to actually save.


When useCallback Genuinely Matters: The Memo Dependency Chain

useCallback matters most specifically when its output (the stable function reference) is consumed by something that depends on reference stability — most commonly, a child component wrapped in React.memo, or a dependency array of another hook like useEffect or useMemo where an unstable function reference would cause that other hook to re-run unnecessarily on every render.

If a function is not being passed to a memoized child component and is not used in another hook’s dependency array, wrapping it in useCallback provides no genuine benefit, since there is no actual reference-stability-dependent consumer that would benefit from this stability.


A Common Mistake: Overusing Both Hooks Defensively

Similar to the blanket React.memo application discussed in our other guide, I have seen codebases wrap nearly every function in useCallback and every computed value in useMemo defensively, without verifying whether the specific function or value actually has a consumer that benefits from this stability or calculation caching.

This defensive overuse adds genuine cognitive overhead (more dependency arrays to maintain correctly) and minor performance overhead (the hooks themselves are not entirely free) without corresponding benefit for the cases where no actual consumer depends on the stability or caching being provided.


Getting the Dependency Array Wrong Silently Breaks Both Hooks

This is worth emphasizing directly, since it is a genuinely common source of bugs. If you omit a dependency that the memoized function or value actually relies on, you get a stale closure — the memoized function or value continues using outdated values from whenever it was last actually recalculated, rather than reflecting current state, producing bugs that can be genuinely confusing to diagnose since the code appears correct at a glance.

// Bug: missing 'count' in dependency array means this callback
// always references the count value from the first render
const handleClick = useCallback(() => {
  console.log(count);
}, []); // should include count

Using the eslint-plugin-react-hooks exhaustive-deps rule, which flags missing dependencies, catches many of these mistakes automatically and is worth having enabled in any React project using these hooks.


A Quick Reference for Choosing Between Them

Situation Use
Caching an expensive calculation’s result useMemo
Providing a stable function reference for a memoized child useCallback
Stabilizing a function used in another hook’s dependency array useCallback
The value or function has no consumer depending on stability Neither — skip memoization

What Resolved the Confusion in My Code Review

Once we walked through this distinction directly — useMemo caches what a function returns, useCallback caches the function itself — the developer’s specific misapplication became clear, and we corrected both instances to use the genuinely appropriate hook for each actual purpose, verifying with the Profiler that the corrected version actually achieved the intended reference stability for the downstream memoized component that had originally motivated this optimization attempt.

Are you trying to optimize a specific component or fix a memoization issue that is not working as expected? Describe your situation and I can help you think through which hook actually fits your need.