Skip to content

useMemo and useCallback Patterns Interview Questions & Answers

15 questions Updated 2026-06-24 Share:

React useMemo and useCallback interview questions — memoization patterns, dependency arrays, when to use each hook, referential stability, and common pitfalls.

Read the in-depth guideReact useMemo and useCallback Patterns — A Complete Guide(opens in new tab)
15 of 15

useMemo is a React hook that caches the result of a computation between renders. It re-runs the factory function only when one of its listed dependencies changes; otherwise it returns the previously cached value.

function ProductList({ products, filterText }) {
  // Without useMemo: filtered every render even if products/filterText
  // haven't changed
  const filtered = useMemo(
    () => products.filter(p => p.name.includes(filterText)),
    [products, filterText] // only re-compute when these change
  )

  return filtered.map(p => <ProductRow key={p.id} product={p} />)
}

The problem it solves is twofold. First, it avoids re-running expensive computations on every render. Second, it preserves referential identity of objects and arrays so downstream React.memo comparisons and useEffect dependency checks don't fire unnecessarily.

Rule of thumb: Only reach for useMemo when the computation is measurably expensive or when referential stability of the returned value is required by a consumer — not as a default wrapping strategy.

useCallback memoizes a function reference so the same function object is returned across renders as long as its dependencies stay the same. It is syntactic sugar over useMemo returning a function.

// These two are equivalent:
const handleClick = useCallback(() => {
  doSomething(id)
}, [id])

const handleClick = useMemo(() => () => {
  doSomething(id)
}, [id])

The key difference in intent:

  • useMemo — memoize a computed value (object, array, number…)
  • useCallback — memoize a function definition so its reference stays stable

Both return a cached result; useCallback is just the idiomatic form when that result is a function.

Rule of thumb: Use useCallback when you need a stable function reference to pass as a prop to a memoized child or as a dependency inside a useEffect — not to speed up the call itself.

The dependency array is the second argument to both hooks. React compares each element of the array to its previous value using Object.is after every render. If any element changed, the cached value is discarded and the factory function re-runs.

const total = useMemo(() => {
  return items.reduce((sum, item) => sum + item.price, 0)
}, [items]) // re-compute only when `items` reference changes

// Common mistakes:
// [] — never re-computes (stale data if items changes)
// no array — re-computes every render (defeats memoisation)
// [items.length] — misses mutations that don't change length

React's ESLint plugin (eslint-plugin-react-hooks) enforces the exhaustive-deps rule, which flags any value used inside the factory that is missing from the dependency array. This prevents stale closure bugs at the cost of occasionally breaking memoisation when a dependency changes too often.

Rule of thumb: Always list every reactive value read inside the factory; rely on eslint-plugin-react-hooks to catch omissions automatically.

Referential stability means an object or function returns the same memory reference across renders. In JavaScript, two objects with identical contents are still different values under Object.is, which is what React uses for prop comparison and dependency checks.

function Parent() {
  // NEW object reference every render — breaks child memo
  const style = { color: 'red' }

  // STABLE reference — child memo works
  const style = useMemo(() => ({ color: 'red' }), [])

  return <MemoizedChild style={style} />
}

const MemoizedChild = React.memo(function Child({ style }) {
  console.log('render') // fires every render without useMemo above
  return <div style={style}>hello</div>
})

Without referential stability, React.memo always sees "new" props, useEffect dependency checks always see "new" dependencies, and downstream useMemo/useCallback hooks always invalidate — making all memoisation downstream pointless.

Rule of thumb: Stabilise object and array props with useMemo and function props with useCallback before passing them into React.memo components or useEffect dependencies.

Memoisation has a cost: React must store the previous value, run the dependency comparison on every render, and keep a closure alive in memory. For cheap operations, this overhead exceeds the saving.

// Bad — wrapping a trivial computation
const doubled = useMemo(() => count * 2, [count])

// Good — just compute it inline
const doubled = count * 2

// Bad — memoizing a callback that is never passed to a memo'd child
const handleClick = useCallback(() => setOpen(true), [])

// Good — inline is fine if the parent re-renders anyway
const handleClick = () => setOpen(true)

Avoid useMemo/useCallback when:

  • The computation is fast (arithmetic, simple string ops)
  • The dependency changes on every render anyway
  • The child receiving the value is not wrapped in React.memo
  • You're in early development — premature optimisation obscures intent

Rule of thumb: Profile first with React DevTools Profiler; add memoisation only where you can measure a render-time win, not preemptively everywhere.

There is no fixed rule, but the common benchmark is >1 ms in the profiler during normal usage. React's own docs suggest a rough test: wrap the call in console.time / console.timeEnd and see if it consistently exceeds 1 ms on mid-range hardware.

// Quick check in development
console.time('filter')
const result = hugeList.filter(expensivePredicate)
console.timeEnd('filter') // e.g. "filter: 3.4 ms" → worth memoizing

// Production pattern
const filtered = useMemo(
  () => hugeList.filter(expensivePredicate),
  [hugeList, expensivePredicate]
)

Common genuinely expensive operations: filtering/sorting large arrays (10 000+ items), heavy string processing, recursive tree traversals, and cryptographic hashes. Cheap operations that look expensive but aren't: .map over <500 items, simple arithmetic, and string concatenation.

Rule of thumb: If console.time shows <1 ms consistently, skip useMemo — the hook overhead may actually be slower than recomputing.

React.memo skips re-rendering a child when all props are shallowly equal. For that to work, object and function props must be referentially stable — which is exactly what useMemo and useCallback provide in the parent.

const Chart = React.memo(function Chart({ data, onHover }) {
  console.log('Chart render')
  return <canvas /> // expensive canvas draw
})

function Dashboard({ rawData }) {
  // Without these, Chart re-renders on every Dashboard render
  const data = useMemo(
    () => transformForChart(rawData), // stable reference
    [rawData]
  )
  const onHover = useCallback(
    (point) => showTooltip(point),
    [] // no deps — stable forever
  )

  return <Chart data={data} onHover={onHover} />
}

The three work as a unit: React.memo is the gate; useMemo and useCallback are what keep the keys from changing unnecessarily. Without stable props, React.memo is effectively a no-op.

Rule of thumb: Whenever you wrap a component in React.memo, audit its parent for inline objects/functions and stabilise them with useMemo/useCallback.

A stale closure occurs when a useCallback (or useMemo) captures a value from an earlier render because the dependency array was incomplete. The function "closes over" the old value and continues using it even after the real value has changed.

function Counter() {
  const [count, setCount] = useState(0)

  // BUG: count is captured at 0 and never updated
  const logCount = useCallback(() => {
    console.log(count) // always logs 0
  }, []) // ← missing `count` dependency

  // FIX 1: add count to deps (new function ref when count changes)
  const logCount = useCallback(() => {
    console.log(count)
  }, [count])

  // FIX 2: use functional updater when only setting state
  const increment = useCallback(() => {
    setCount(prev => prev + 1) // no stale closure — reads latest
  }, [])

  return <button onClick={logCount}>Log {count}</button>
}

The ESLint exhaustive-deps rule catches most stale closures at lint time. For callbacks that need to always read the latest value without changing reference, consider the ref-callback pattern: store the latest function in a ref and call it from a stable wrapper.

Rule of thumb: Never omit a dependency to "keep the function stable" — either add it honestly or use useReducer / functional updates to remove the dependency legitimately.

Three situations call for object memoisation:

  1. Derived data — transform props into a new object for rendering
  2. Config objects — pass stable option bags to child components
  3. Context values — prevent all consumers from re-rendering
function UserCard({ user, theme }) {
  // 1. Derived data
  const displayName = useMemo(
    () => `${user.firstName} ${user.lastName}`.trim(),
    [user.firstName, user.lastName]
  )

  // 2. Stable config object for a memoized child
  const chartOptions = useMemo(
    () => ({ color: theme.primary, legend: false }),
    [theme.primary]
  )

  // 3. Context value — prevents all consumers re-rendering
  const ctx = useMemo(
    () => ({ user, displayName }),
    [user, displayName]
  )

  return (
    <UserContext.Provider value={ctx}>
      <MemoizedChart options={chartOptions} />
    </UserContext.Provider>
  )
}

Avoid memoizing objects whose dependencies change on every render — you get the cost of comparison with none of the saving.

Rule of thumb: Memoize objects when you control neither the re-render frequency of the parent nor the comparison logic of the consumer — context providers and React.memo children are the canonical cases.

The answer depends entirely on whether the callback is passed to a memoized child or used as a useEffect dependency.

function Form() {
  const [value, setValue] = useState('')

  // INLINE — fine: onChange is not passed to a memo'd child
  // and causes no extra effects
  return (
    <input
      onChange={e => setValue(e.target.value)} // new ref each render
      value={value}
    />
  )
}

function SearchPanel({ onSearch }) {
  // EXTRACTED — necessary: ResultList is React.memo'd
  const handleSearch = useCallback(() => {
    onSearch(query)
  }, [onSearch, query])

  return <MemoizedResultList onSearch={handleSearch} />
}

Inline lambdas are idiomatic and readable for event handlers on DOM elements. Extracting with useCallback adds noise and overhead when the consumer doesn't benefit from a stable reference.

Rule of thumb: Default to inline; extract with useCallback only when the callback flows into a React.memo boundary or a useEffect / useMemo dependency list.

A custom hook's returned functions become dependencies for any useEffect or useCallback in the consuming component. If the hook returns a new function reference on every call, the consumer is forced to either omit it from deps (stale closure risk) or accept spurious effect re-runs.

// Bad — new reference every render
function useSearch(query) {
  const search = () => fetchResults(query) // new fn each time
  return { search }
}

// Good — stable reference, updates only when query changes
function useSearch(query) {
  const search = useCallback(
    () => fetchResults(query),
    [query]
  )
  return { search }
}

// Consumer can now safely list `search` as a dep
function SearchBox({ query }) {
  const { search } = useSearch(query)

  useEffect(() => {
    search() // no stale closure, no spurious re-runs
  }, [search])
}

This is why popular hook libraries (React Query, SWR, Zustand) memoize every returned function — it makes composition predictable without requiring callers to know the hook's internals.

Rule of thumb: Treat functions returned from custom hooks the same as public API — wrap them in useCallback so consumers can safely include them in dependency arrays.

Every time a Context.Provider re-renders, all consumers re-render unless the context value is referentially the same object. Without useMemo, a provider's value object is recreated on every parent render, making the context update cascade unavoidable.

// Bad — new object on every Parent render
function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  return (
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  )
}

// Good — stable object; consumers only re-render when user changes
function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const value = useMemo(
    () => ({ user, setUser }),
    [user] // setUser is stable from useState, safe to omit
  )
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  )
}

For large apps, splitting context into a read context and a write context (each with its own useMemo value) further reduces re-renders: write-only consumers don't re-render when the user data changes.

Rule of thumb: Always wrap a context value in useMemo in providers that live high in the tree — the blast radius of a needless update is proportional to how many consumers are below it.

Memoisation can introduce subtle bugs when the dependency array is wrong or when the memoized value is mutated instead of replaced.

// PITFALL 1: mutating the cached value
const items = useMemo(() => [], [])
items.push(newItem) // ← mutates; useMemo won't notice, UI goes stale

// Fix: always return a new array
const items = useMemo(() => [...baseItems, newItem], [baseItems, newItem])

// PITFALL 2: object identity as a dep (always "new")
const options = { limit: 10 }
const result = useMemo(() => query(options), [options]) // re-runs every render

// Fix: depend on primitives, not the object
const result = useMemo(() => query({ limit: 10 }), []) // or [limit]

// PITFALL 3: async factory (useMemo is synchronous only)
const data = useMemo(async () => fetchData(), []) // returns a Promise, not data

// Fix: use useEffect + useState for async work

These pitfalls share a root cause: treating useMemo as smarter than it is. It only tracks the dependency array — it cannot detect mutation or understand async semantics.

Rule of thumb: Treat memoized values as immutable; never mutate them in place, depend only on primitives or already-stable references, and keep factory functions synchronous.

The ref-callback pattern (sometimes called "latest ref") keeps a useCallback reference stable while still reading the latest closure values. It solves the tension between "stable function" and "always current data".

function useLatestCallback(fn) {
  const ref = useRef(fn)
  // keep the ref up to date on every render (safe: layout effect)
  useLayoutEffect(() => { ref.current = fn })
  // return a stable wrapper that delegates to the latest fn
  return useCallback((...args) => ref.current(...args), [])
}

function SearchBox({ onSearch }) {
  // onSearch may change often, but handleSearch is always stable
  const handleSearch = useLatestCallback(onSearch)

  useEffect(() => {
    const id = setInterval(handleSearch, 5000)
    return () => clearInterval(id)
  }, [handleSearch]) // no stale closure, no interval reset on each render
}

This pattern is useful for event handlers and interval callbacks that must be stable but need to capture the very latest state or props. React's upcoming useEffectEvent hook formalises this pattern in the framework itself.

Rule of thumb: Reach for the ref-callback pattern when useCallback forces an awkward choice between adding a fast-moving dep (breaking stability) and omitting it (creating a stale closure).

No. Native DOM event handlers (on <button>, <input>, etc.) do not benefit from useCallback. React attaches DOM events at the root via event delegation — the function reference you pass is never directly stored on the DOM node, and changing it between renders has zero cost.

function Counter() {
  const [count, setCount] = useState(0)

  // Unnecessary — the <button> doesn't care about reference stability
  const handleClick = useCallback(
    () => setCount(c => c + 1),
    []
  )

  // Equivalent and simpler
  const handleClick = () => setCount(c => c + 1)

  // useCallback IS needed when handleClick flows into a memo'd child
  return (
    <>
      <button onClick={handleClick}>{count}</button>
      <MemoizedExpensiveChild onAction={handleClick} />
    </>
  )
}

The second <MemoizedExpensiveChild> case justifies useCallback because React.memo's prop comparison is what creates the stability requirement — the DOM element does not.

Rule of thumb: If the only consumer of a callback is a plain DOM element (not a React.memo component), skip useCallback — it adds complexity with no measurable benefit.

More ways to practice

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

or
Join our WhatsApp Channel