useCallback vs useMemo — when they actually help

The precise difference between the two hooks, what 'referential stability' means, and the honest truth about when memoization helps vs when it's just noise.

must medium ⏱ 18 min useCallbackuseMemomemoizationreferential-equalityperformance
Mastery:
Why interviewers ask this
This is the most commonly misunderstood hook pair. Interviewers ask because overuse of useMemo/useCallback is a widespread code smell, and knowing when NOT to use them is the senior signal.

Both hooks memoize things across renders. The distinction is what they memoize:

  • useMemo — memoizes a computed value (the result of calling a function).
  • useCallback — memoizes a function itself (so its identity is stable).
// useMemo: cache an expensive computed result
const sorted = useMemo(
  () => [...items].sort(compareByDate), // fn runs only when items changes
  [items]
);

// useCallback: keep a function's identity stable across renders
const handlePress = useCallback(
  (id) => dispatch({ type: 'SELECT', id }),
  [dispatch]
);

// useCallback is exactly equivalent to:
const handlePress = useMemo(
  () => (id) => dispatch({ type: 'SELECT', id }),
  [dispatch]
);

Why referential equality matters

JavaScript functions and objects are compared by reference, not by value. On every render, a function literal creates a new function object:

const a = () => {};
const b = () => {};
console.log(a === b); // false — different objects in memory

This matters because:

  1. React.memo compares props with shallow equality (===). A new function reference every render means memo always sees “props changed” → no bail-out.
  2. Effect dependency arrays use Object.is (essentially ===). A new function reference every render means the effect re-runs every render.

The only situations where useCallback helps

Situation 1: Passing a callback to a memoized child

function Parent() {
  // Without useCallback: new function every render → memo is useless
  // With useCallback: same function reference → memo can bail out
  const handleSelect = useCallback((id) => setSelected(id), []);
  return <MemoizedList onSelect={handleSelect} />;
}
const MemoizedList = React.memo(({ onSelect }) => { /* ... */ });

Situation 2: A function is in an effect’s dependency array

// Without useCallback: fetchData is new every render → effect re-runs every render → infinite loop
const fetchData = useCallback(async () => {
  const data = await api.get('/items');
  setItems(data);
}, []); // stable reference

useEffect(() => {
  fetchData();
}, [fetchData]); // effect only re-runs when fetchData changes

When useCallback does NOT help (most of the time)

// Scenario: inline event handlers on non-memoized children
// The child re-renders because the PARENT re-renders, regardless of the prop value
function Parent() {
  const handleClick = useCallback(() => console.log('click'), []);
  return <div onClick={handleClick}>Click me</div>; // div is not React.memo'd
  // useCallback here adds cost (the comparison) with zero benefit
}

If the child is:

  • A native host component (<div>, <View>, <Text>) — host components don’t do prop comparisons
  • A React component that’s NOT wrapped in React.memo

…then useCallback does nothing useful and just adds overhead.

When useMemo helps vs when it’s premature

// GOOD: genuinely expensive computation
const processedData = useMemo(() => {
  return hugeDataset.flatMap(item =>      // O(n log n) sort
    item.entries.filter(e => e.active)
  ).sort(compareByScore);
}, [hugeDataset]);

// GOOD: stable object reference for memoized child
const options = useMemo(
  () => ({ locale: 'en-US', currency: 'USD' }),
  [] // never recreated — stable for downstream React.memo
);

// BAD: trivial computation — memo cost > compute cost
const doubled = useMemo(() => count * 2, [count]); // just write count * 2

// BAD: the "memoize everything" habit — costs memory and comparison overhead
const message = useMemo(() => `Hello, ${name}!`, [name]); // not worth it

The decision flowchart

Is the computation expensive (>1ms, large arrays, complex transforms)?
  YES → useMemo it

Is the function passed to a React.memo-wrapped child?
  YES → useCallback it

Is the function in a useEffect dependency array?
  YES → useCallback it

Otherwise?
  Write it inline. Memoize after profiling shows it's a problem.

Say it out loud
useMemo caches a computed value; useCallback caches a function’s identity — they’re the same mechanism. Memoization only helps when a child uses React.memo and you’re passing it a prop, or when a function is in an effect’s deps array. For everything else, the memo overhead costs more than re-creating the value. The rule: measure first, then memoize the real hotspot. Wrapping every callback in useCallback is a code smell, not a performance win.”

Likely follow-up questions
  • What's the difference between useCallback and useMemo?
  • Why doesn't useCallback on every function improve performance?
  • When does React.memo actually skip a re-render?
  • What is referential equality and why does it matter?
  • When should you NOT use useMemo?

References