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:
React.memocompares props with shallow equality (===). A new function reference every render meansmemoalways sees “props changed” → no bail-out.- 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.”