Skip to content

React · Hooks

React useCallback & useMemo — Complete Interview Guide

7 min read Updated 2026-06-23 Share:

Practice useCallback & useMemo interview questions

The problem these hooks solve

Every render of a React component runs the function body from top to bottom. Every function and object defined inside that body is a brand new reference each time.

function Parent() {
  const options = { color: 'blue' }  // new object every render
  const handleClick = () => save()   // new function every render
  return <Child options={options} onClick={handleClick} />
}

Most of the time this is fine — React's reconciler is fast, and a new reference doesn't cause visible slowness. But it breaks two specific optimization mechanisms: React.memo and effect dependency arrays. useCallback and useMemo solve exactly this by giving you stable references that only change when their dependencies change.

useCallback

useCallback(fn, deps) returns a memoized function. On each render it compares deps with the previous render's deps using Object.is. If nothing changed, it returns the exact same function reference from last time.

const handleSort = useCallback((column) => {
  setSort(column)
}, []) // deps: empty — sort setter is always the same

This is purely about referential stability — the function still runs normally when called. The optimization is that handleSort === previousHandleSort between renders where the deps didn't change.

useMemo

useMemo(() => computation, deps) caches the return value of a computation. It only re-runs the computation when a dependency changes.

const filtered = useMemo(
  () => products.filter(p => p.active).sort((a, b) => a.price - b.price),
  [products]
)

Without useMemo, the filter and sort run on every render — even on a keystroke that updates completely unrelated state. With it, the sorted result is reused until products changes.

The relationship between the two

useCallback(fn, deps) is exactly useMemo(() => fn, deps). They're the same mechanism: one is specialized for the "the value is a function" case.

const fn = useCallback(() => compute(x), [x])
// identical to:
const fn = useMemo(() => () => compute(x), [x])

In practice, prefer the purpose-fit name: useCallback for stable function references, useMemo for stable computed values.

Why referential equality matters

JavaScript compares objects and functions by reference (memory address), not by content. Two identically written functions are never equal to each other:

(() => {}) === (() => {})  // false — different objects in memory
const fn = () => {}
fn === fn                  // true — same reference

This matters because React.memo bails out of a re-render only when every prop is Object.is-equal to the previous render. An inline function prop fails that check on every render, defeating the memo.

Combining React.memo with useCallback

The pattern that actually works: React.memo on the child component, useCallback for the function props passed from the parent.

// Child: skip re-renders when props are equal
const Row = React.memo(function Row({ item, onDelete }) {
  return <div>{item.name} <button onClick={() => onDelete(item.id)}>x</button></div>
})

// Parent: stable function reference so memo can work
function List({ items }) {
  const handleDelete = useCallback(id => {
    setItems(prev => prev.filter(i => i.id !== id))
  }, [])
  return items.map(item => <Row key={item.id} item={item} onDelete={handleDelete} />)
}

Without useCallback, handleDelete is a new function each render → Row's memo never hits → every item re-renders on any list change.

Stabilizing effect dependencies

Functions defined inside a component are new references every render. If an effect depends on such a function, it re-runs on every render — the very problem useEffect deps are supposed to prevent.

// without useCallback: refetch fires every render
function Page({ userId }) {
  const fetchUser = () => api.get(userId)
  useEffect(() => { fetchUser().then(setUser) }, [fetchUser]) // new fn each render
}

// with useCallback: refetch fires only when userId changes
const fetchUser = useCallback(() => api.get(userId), [userId])
useEffect(() => { fetchUser().then(setUser) }, [fetchUser])

An alternative that's often cleaner: move the function inside the effect so it's not in the dependency array at all.

Stabilizing object props with useMemo

The same referential-equality issue applies to objects and arrays. Wrap them in useMemo to preserve the reference when the content hasn't changed.

// new object every render -> memoized child always re-renders
<Chart config={{ color: theme, size }} />

// stable reference -> child re-renders only when theme or size change
const config = useMemo(() => ({ color: theme, size }), [theme, size])
<Chart config={config} />

For context providers, memoizing the value object is the fix for "all consumers re-render on every Provider render."

When NOT to memoize

This is what most developers miss: useCallback and useMemo have real costs.

Every render, React must:

  1. Read the stored deps array.
  2. Compare each dep with Object.is.
  3. Return the cached value or re-run the computation.

For trivial computations, this overhead exceeds the cost of just re-running the computation. The React team has publicly said that the overhead of memoization can make performance worse for simple cases.

// overhead > savings
const label = useMemo(() => `Hello ${name}`, [name])

// just compute it
const label = `Hello ${name}`

Memoize when you have evidence of a problem (profiling with React DevTools Profiler), not as a default practice. The three cases worth memoizing:

  1. Genuinely expensive computation — filtering/sorting thousands of items.
  2. Stable reference for a memoized childReact.memo component receiving object or function props.
  3. Effect dependency stabilization — function or object in a useEffect dep array.

Stale closure: the missing dependency bug

Omitting a value from the deps array means the memoized function/value closes over a stale snapshot of that value from the render where it was last computed.

// user is omitted -> greeting always uses the initial user.name
const greeting = useMemo(() => `Hi ${user.name}`, []) // bug: stale forever

// correct
const greeting = useMemo(() => `Hi ${user.name}`, [user.name])

The react-hooks/exhaustive-deps ESLint rule catches this. Never suppress the rule to "cache something forever" — if you genuinely want a value from a specific moment in time, store it in a useRef.

Measuring before optimizing

The correct workflow:

  1. Notice a perceived performance issue.
  2. Open React DevTools Profiler and record the interaction.
  3. Identify which component is the hot path and why.
  4. Apply useMemo/useCallback/React.memo surgically.
  5. Record again to confirm the improvement.

Wrapping everything in useCallback "just in case" adds code, slows down renders for most code paths, and makes dependency arrays harder to maintain.

Common interview questions at a glance

  • What does useCallback return? A memoized function reference, identical across renders until a dependency changes.
  • What does useMemo return? A memoized computed value, recomputed only when deps change.
  • How do they differ? Same mechanism; useCallback caches a function, useMemo caches any value (often the result of calling a function).
  • Why doesn't React.memo work if I don't useCallback? Function props are new references each render; memo sees them as changed.
  • When should you memoize? When profiling shows a real problem — not by default.
  • What's the stale closure risk? Omitting a dep means the memoized value reads a frozen old snapshot of that dep forever.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel