Skip to content

useCallback & useMemo Interview Questions & Answers

15 questions Updated 2026-06-23 Share:

React useCallback and useMemo interview questions — referential stability, when to memoize, dependency arrays, performance pitfalls, and React.memo interaction.

Read the in-depth guideReact useCallback & useMemo — Complete Interview Guide(opens in new tab)
15 of 15

useCallback returns a memoized version of a function that only changes when one of its dependencies changes. Calling it with the same dependencies returns the exact same function reference across renders.

const handleClick = useCallback(() => {
  doSomething(id)
}, [id]) // new function only when `id` changes

Without useCallback, a new function is created on every render. With it, the reference stays stable until id changes — which matters when passing the function to a memoized child or an effect dependency.

Rule of thumb: useCallback(fn, deps) is useMemo(() => fn, deps).

useMemo caches the return value of a computation and recomputes it only when its dependencies change.

const sorted = useMemo(
  () => [...items].sort(compare),
  [items] // recompute only when items changes
)

On renders where items hasn't changed, React skips calling the sort and returns the cached result. Use it for expensive derivations from props or state.

They solve the same problem — referential stability across renders — for different output types:

  • useMemo memoizes the return value of a function (any value).
  • useCallback memoizes the function itself (a specific case of useMemo).
// these two are equivalent
const fn = useCallback(() => compute(x), [x])
const fn = useMemo(() => () => compute(x), [x])

// useMemo for a computed value
const result = useMemo(() => compute(x), [x])
// useCallback for a stable function reference
const handler = useCallback(() => post(x), [x])

In practice: useCallback for functions, useMemo for values.

JavaScript compares objects and functions by reference (memory address), not by content. Two identically-written functions are not equal unless they're the same reference.

(() => {}) === (() => {})  // false — different references
const fn = () => {}
fn === fn                  // true — same reference

This is why a new function created each render makes a child re-render even when nothing logically changed: React.memo sees a new prop reference and treats it as a change. useCallback returns the same reference, keeping memoization intact.

React.memo wraps a component and skips re-rendering when all its props are reference-equal to the last render. If you pass an unstable function prop (new reference each render), React.memo still re-renders — defeating the memo.

const Button = React.memo(function Button({ onClick }) {
  return <button onClick={onClick}>click</button>
})

function Parent() {
  // new function each render -> Button re-renders despite memo
  return <Button onClick={() => console.log('hi')} />

  // stable function -> memo works
  const handleClick = useCallback(() => console.log('hi'), [])
  return <Button onClick={handleClick} />
}

The pattern: React.memo on the child + useCallback on the function prop passed from the parent.

Memoize when you have a measurable performance problem, not preemptively. The three cases worth memoizing:

  1. Expensive computation — sorting/filtering thousands of items, complex math.
  2. Stable reference for a memoized childReact.memo child that receives functions or objects as props.
  3. Effect dependency stabilization — an object/function listed in useEffect deps that would otherwise re-run the effect every render.
// worth it: sort runs O(n log n) on a large list
const sorted = useMemo(() => items.sort(compare), [items])

// not worth it: trivial, memo overhead exceeds savings
const doubled = useMemo(() => x * 2, [x])

Profile first. Every useMemo/useCallback adds its own comparison cost and cognitive overhead — it's not free.

useMemo and useCallback have their own cost: React must store the cached value, compare dependency arrays on each render, and manage garbage collection. Memoizing cheap operations can make performance worse because the overhead exceeds the computation.

// overhead > savings for trivial computation
const label = useMemo(() => `Hello ${name}`, [name])
// just compute it: const label = `Hello ${name}`

Additionally, memoization adds cognitive load — developers reading the code must reason about whether the deps array is correct. Default to plain computation; add memoization only when profiling shows a real problem.

The memoized value/function closes over a stale value — the one from the render where it was last created. It never sees later updates to the omitted dep.

// user is omitted from deps -> greeting always shows the initial user.name
const greeting = useMemo(() => `Hi ${user.name}`, []) // stale closure!

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

The react-hooks/exhaustive-deps eslint rule catches this. Never suppress it to "cache something forever" — use a useRef if you intentionally want a value frozen after mount.

A function defined in a component is recreated each render (new reference). If an effect depends on it, the effect re-runs every render. useCallback gives the function a stable reference that only changes when its own deps do.

// without useCallback: new function each render -> effect fires every render
function fetchUser() { return api.get(userId) }
useEffect(() => { fetchUser().then(setUser) }, [fetchUser])

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

Alternatively, move the function inside the effect to remove the dependency entirely — that's often cleaner.

Wrap the object in useMemo so it keeps the same reference until its inputs change. Without it, every render creates a new object even if the content is identical — causing memoized children and dependent effects to fire.

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

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

This is the object-version of useCallback. Prefer depending on the individual primitives directly when possible — it's simpler.

Compute the derived value with useMemo, depending on the raw data and any sort/ filter parameters. The computation is skipped on renders where none of those inputs changed.

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

Without useMemo, the filter and sort run on every keystroke, click, and hover that triggers a re-render — even when products and activeCategory haven't changed.

Each call to useCallback/useMemo on every render:

  1. Reads the stored deps array.
  2. Compares each dep with Object.is.
  3. Either returns the cached value or re-runs the computation.

For a tiny list or a trivial function, this comparison can cost more than just re-running the computation. React's own team has noted that memoization is a micro-optimization and can be counter-productive on small values.

// comparison overhead likely exceeds multiplication cost
const n = useMemo(() => x * 2, [x])
// vs just: const n = x * 2

Benchmark before memoizing. Use React DevTools Profiler to identify actual expensive renders, then target them specifically.

No. Inline functions are perfectly fine when:

  • The child is not wrapped in React.memo.
  • The function is not in an effect dependency array.
  • The child re-render is cheap (no deep tree, no expensive computation).
// fine: Button is not memoized, re-render is cheap
<Button onClick={() => setOpen(true)} />

// needed: DataGrid is memoized and re-renders are expensive
const handleSort = useCallback(col => setSort(col), [])
<DataGrid onSort={handleSort} />

Defaulting to useCallback for every handler is premature optimization that adds noise. Add it when you have a real memoization need.

Always prefer useMemo over storing derived data in state. State requires you to keep two values in sync (the source and the derived copy), which can lead to bugs. useMemo derives the value automatically and stays in sync by definition.

// anti-pattern: derived state
const [items, setItems]   = useState([])
const [count, setCount]   = useState(0)
// must remember to call setCount every time setItems changes

// correct: derive in render
const [items, setItems] = useState([])
const count = useMemo(() => items.length, [items])
// or even simpler: const count = items.length (no memo needed here)

For truly expensive derivations, add useMemo; for trivial ones, just compute them inline — no hook needed.

Remove memoization when: (1) profiling shows the memoized path is never hot; (2) the dependency array is so complex or unstable that the memo rarely hits; (3) the memoized value is consumed by a non-memoized child (the stability gains nothing).

// useless: MutableChild re-renders anyway for other reasons
const handler = useCallback(fn, [deps])
<MutableChild onClick={handler} />

// useful: MemoChild skips renders when handler is stable
const handler = useCallback(fn, [deps])
<MemoChild onClick={handler} />

Memoization is not free — when it doesn't help, it only adds confusion. Keep the code simple; add memos with evidence, remove them without regret.

More ways to practice

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

or
Join our WhatsApp Channel