What is the difference between useCallback and useMemo?
useCallback memoizes a function reference; useMemo memoizes a computed value. Same dep-array mechanic, different return type.
Theory
TL;DR
useMemocaches a computed value: filter a list once, reuse the result until deps changeuseCallbackcaches a function reference: same function object every render unless deps change- Core insight:
useCallback(fn, deps)is literallyuseMemo(() => fn, deps) - Need a stable function ref for a child prop?
useCallback. Need an expensive computed value?useMemo - For cheap operations, skip both. Hook overhead adds up fast.
Quick example
import { useState, useMemo, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// useMemo: recomputes only when count changes
const doubled = useMemo(() => count * 2, [count]);
// useCallback: same function reference unless deps change
const handleClick = useCallback(() => setCount(c => c + 1), []);
return <button onClick={handleClick}>Count: {doubled} | other: {other}</button>;
}Change other and doubled does not recompute. Change count and doubled recalculates, but handleClick stays the same reference.
Key difference
useMemo runs your factory and stores whatever it returns: a number, string, object, array. useCallback stores the factory function itself without calling it. So if your factory returns a function, useMemo gives you a new function reference each time (unstable), while useCallback gives you the stable reference you actually need for React.memo child optimization.
When to use
- Filtering or sorting a large array on every render?
useMemoon the result. - Passing a callback as a prop to a
React.memochild?useCallbackto keep the reference stable. - Inline function in a useEffect dep array?
useCallbackto prevent infinite loops. count * 2orname.toUpperCase()? Neither. Compute inline.
Comparison table
| Aspect | useMemo | useCallback |
|---|---|---|
| Returns | Computed value (any type) | Function reference |
| Factory runs | Only when deps change | Only when deps change |
| Primary use | Expensive calculations | Stable callback props |
| With React.memo | Memoize object/array props | Memoize function props |
| Equivalent to | useMemo(() => val, deps) | useMemo(() => fn, deps) |
| When to skip | Cheap operations | Function not passed to any child |
How it works internally
React stores the memoized result in the fiber node's memoizedState. On each re-render it runs a shallow comparison (Object.is) on each dep. If all deps match, it returns the cached value or function without calling the factory. This is why object or array deps cause constant recomputes: {} never equals {} by reference.
Common mistakes
Missing deps (stale closure):
// Wrong: b is used but not in deps
const sum = useMemo(() => a + b, [a]); // Always a + 2, ignores b updates
// Fix
const sum = useMemo(() => a + b, [a, b]);eslint-plugin-react-hooks with exhaustive-deps catches this automatically. Add it to every React project.
useCallback when no child receives the function:
// Pointless: handleClick is not passed anywhere as a prop
const handleClick = useCallback(() => setCount(c => c + 1), []);
return <button onClick={handleClick}>Click</button>;I've seen codebases where useCallback was added to every handler after someone read an optimization guide. Profiler showed no difference, just extra overhead and harder-to-spot dep bugs. If the function is not passed to a memoized child or used in a dep array, inline it.
Object or array in deps without memoization:
// Recomputes every render: options is a new reference each time
const result = useMemo(() => compute(options), [options]);
// Fix: memoize the dep too
const stableOptions = useMemo(() => ({ limit: 10 }), []);
const result = useMemo(() => compute(stableOptions), [stableOptions]);Real-world usage
- Search/filter UI:
useMemoto filter 1000+ items without re-filtering on unrelated state changes - TanStack Query: wraps query results in
useMemofor stable references - Custom hooks:
useCallbackon returned functions so consumers don't re-render unnecessarily - Redux selectors: stable action creators via
useCallbackpatterns
Follow-up questions
Q: What happens if you put an object literal directly in the dep array?
A: Shallow compare fails every render. {} === {} is false, so the hook recomputes constantly. Wrap the object in its own useMemo or destructure it to primitives.
Q: If useCallback(fn, deps) equals useMemo(() => fn, deps), why does React have both?
A: Readability. useCallback signals intent: this is a stable callback. useMemo signals: this is a cached value. Same engine, different semantics.
Q: Can memoization make performance worse?
A: Yes. Each hook call allocates memory and runs dep comparisons. For trivial operations like count * 2, that overhead beats the cost of just recalculating. Measure with React DevTools Profiler before adding memos.
Q: Why does the factory run twice in React 18 Strict Mode?
A: React intentionally double-invokes factories in dev to surface side effects in code that should be pure. Production runs it once.
Q: (Senior) How does React 19 change this picture?
A: The React Compiler can automatically insert memoization where it helps, reducing the need to write useMemo and useCallback by hand. The underlying mechanism stays the same.
Examples
Filtering a list with useMemo and useCallback
function TodoList({ todos, filter }) {
// Without useMemo: re-filters all todos on every parent re-render
const visibleTodos = useMemo(() =>
todos.filter(todo =>
todo.text.toLowerCase().includes(filter.toLowerCase())
),
[todos, filter]
);
// Stable ref: memoized TodoItem children skip re-renders
const handleDelete = useCallback((id) => {
console.log('Deleting todo', id);
}, []);
return visibleTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} onDelete={handleDelete} />
));
}visibleTodos recomputes only when todos or filter changes. handleDelete keeps the same reference, so memoized TodoItem children skip re-renders when the parent updates for unrelated reasons.
Stale closure bug in deps
function BadSum() {
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// Bug: b is missing from deps
const sum = useMemo(() => a + b, [a]);
// After b updates to 3, sum still shows a + 2
return (
<>
<button onClick={() => setB(b => b + 1)}>B: {b}</button>
<div>Sum: {sum}</div>
</>
);
}Clicking B increments b in state, but the memo stays stale because b is not in the dep array. Fix: [a, b]. The exhaustive-deps lint rule flags this the moment you save the file.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.