Skip to main content

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

  • useMemo caches a computed value: filter a list once, reuse the result until deps change
  • useCallback caches a function reference: same function object every render unless deps change
  • Core insight: useCallback(fn, deps) is literally useMemo(() => 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

jsx
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? useMemo on the result.
  • Passing a callback as a prop to a React.memo child? useCallback to keep the reference stable.
  • Inline function in a useEffect dep array? useCallback to prevent infinite loops.
  • count * 2 or name.toUpperCase()? Neither. Compute inline.

Comparison table

AspectuseMemouseCallback
ReturnsComputed value (any type)Function reference
Factory runsOnly when deps changeOnly when deps change
Primary useExpensive calculationsStable callback props
With React.memoMemoize object/array propsMemoize function props
Equivalent touseMemo(() => val, deps)useMemo(() => fn, deps)
When to skipCheap operationsFunction 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):

jsx
// 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:

jsx
// 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:

jsx
// 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: useMemo to filter 1000+ items without re-filtering on unrelated state changes
  • TanStack Query: wraps query results in useMemo for stable references
  • Custom hooks: useCallback on returned functions so consumers don't re-render unnecessarily
  • Redux selectors: stable action creators via useCallback patterns

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

jsx
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

jsx
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?