Intermediate0 questionsFull Guide

useCallback Hook — Complete React Interview Guide

Learn exactly when useCallback prevents re-renders and when it's wasted overhead — the most misused hook in React.

The Mental Model

Every time a React component renders, every function declared inside it is recreated as a brand new object in memory. Two renders, two different function objects — even if the code is identical. For most components, this is completely fine. The function is used once, the component re-renders, a new function is created, and the old one is garbage collected. No problem. The problem arises when that function is passed as a prop to a memoized child component, or listed as a dependency in a useEffect. In both cases, React compares the current function with the previous one using reference equality. Two different function objects — even with identical code — are never equal by reference. So the memoized child always re-renders, and the effect always re-runs. useCallback solves this by returning the same function reference across renders, only creating a new one when the dependencies change. It doesn't make the function itself faster — it makes the reference stable. The most important thing to know about useCallback: it is useless without React.memo on the child receiving the function, or without the function being in a useEffect dependency array. Those are the only two scenarios where function reference stability actually matters.

The Explanation

The reference stability problem

function Parent() {
  const [count, setCount] = useState(0)
 
  // New function object created on every render
  const handleClick = () => console.log('clicked')
 
  return <Child onClick={handleClick} />
}
 
// Even if Child is wrapped in React.memo:
const Child = React.memo(({ onClick }) => {
  console.log('Child rendered')
  return <button onClick={onClick}>Click</button>
})
 
// Child renders every time Parent renders
// because onClick is always a new function reference

How useCallback fixes it

function Parent() {
  const [count, setCount] = useState(0)
 
  // Same function reference until dependencies change
  const handleClick = useCallback(() => {
    console.log('clicked')
  }, [])  // no deps — never recreated
 
  return (
    <div>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
    </div>
  )
}
 
const Child = React.memo(({ onClick }) => {
  console.log('Child rendered')  // only logs on mount now
  return <button onClick={onClick}>Click</button>
})
// Child no longer re-renders when count changes ✓

useCallback in useEffect dependencies

// Without useCallback — fetchData is new on every render → effect runs every render
function UserDashboard({ userId }) {
  const fetchData = () => fetch('/api/users/' + userId)  // new fn every render
 
  useEffect(() => {
    fetchData()
  }, [fetchData])  // fetchData always "changes" → infinite loop!
}
 
// With useCallback — effect only re-runs when userId changes
function UserDashboard({ userId }) {
  const fetchData = useCallback(() => {
    return fetch('/api/users/' + userId)
  }, [userId])  // new function only when userId changes
 
  useEffect(() => {
    fetchData()
  }, [fetchData])  // ✓ only runs when userId changes
}

Stale callbacks — missing dependencies

If a callback closes over state but doesn't list it in dependencies, the callback becomes stale:

function SearchBox() {
  const [query, setQuery] = useState('')
 
  // ✗ Stale — query is always '' inside handleSearch
  const handleSearch = useCallback(() => {
    searchAPI(query)  // query is stale!
  }, [])  // ← missing query
 
  // ✓ Add query to deps
  const handleSearch = useCallback(() => {
    searchAPI(query)
  }, [query])  // re-created when query changes
 
  // ✓ Alternative — useRef to break closure without deps
  const queryRef = useRef(query)
  useEffect(() => { queryRef.current = query }, [query])
  const handleSearch = useCallback(() => {
    searchAPI(queryRef.current)  // always current
  }, [])  // stable reference
}

When useCallback does NOT help

// ✗ Useless — Child is NOT React.memo'd
function Parent() {
  const handleClick = useCallback(() => doSomething(), [])
  return <Child onClick={handleClick} />  // not memoized
}
function Child({ onClick }) {  // re-renders when Parent re-renders — always
  return <button onClick={onClick}>Click</button>
}
// Child re-renders every time Parent does regardless of handleClick stability.
// useCallback added cost (comparison + cache) with zero benefit.
 
// ✗ Useless — function not in deps array
const fn = useCallback(() => doSomething(), [])
// fn is used as a click handler but not passed to memo'd component or useEffect
// Stable reference does nothing here

The complete decision tree

// Is the function passed to a React.memo child?
// YES → useCallback to prevent unnecessary re-renders
const memoChild = React.memo(Child)
const handler = useCallback(fn, deps)
<memoChild onAction={handler} />
 
// Is the function in a useEffect dependency array?
// YES → useCallback to prevent infinite loops
const fetchFn = useCallback(() => fetch(url), [url])
useEffect(() => { fetchFn() }, [fetchFn])
 
// Is the function passed to useCallback from a custom hook and consumers will put it in deps?
// YES → useCallback inside the hook
function useSearch(query) {
  const search = useCallback(() => fetchResults(query), [query])
  return { search }
}
 
// Everything else? → inline function is fine
<button onClick={() => setCount(c => c + 1)}>{count}</button>

The stable callback pattern — useRef + useCallback

A stable function reference that always calls the latest version of a callback — without listing dependencies:

// Problem: need stable reference BUT callback must always be current
// Solution: ref stores latest fn, stable wrapper calls it
 
function useStableCallback(fn) {
  const fnRef = useRef(fn)
 
  // Always update ref after render (not during — would cause issues)
  useEffect(() => { fnRef.current = fn })
 
  // Stable reference — never changes, always calls latest fn
  return useCallback((...args) => fnRef.current(...args), [])
}
 
// Usage
function Component({ onSuccess }) {
  const stableOnSuccess = useStableCallback(onSuccess)
 
  // Effect never re-runs due to onSuccess changes — stableOnSuccess is always stable
  useEffect(() => {
    const sub = subscribe(event => stableOnSuccess(event))
    return () => sub.unsubscribe()
  }, [stableOnSuccess])
}

React 19 introduces useEffectEvent as the official version of this pattern.

useCallback and the performance cost

useCallback allocates memory, runs comparison checks, and retains the previous function. It adds CPU and memory cost on every render. The net benefit is negative unless the cost of re-rendering the child component (or re-running the effect) is greater than the memoization overhead.

// Measuring the break-even:
// useCallback cost: ~0.5-2ms per render (comparison + cache)
// Prevented child re-render cost: depends on child complexity
 
// For a simple button: useCallback costs MORE than the re-render
// For an expensive chart component: useCallback saves 50ms+ per render

Common Misconceptions

⚠️

Many developers think useCallback makes the function itself run faster — but it does nothing to the function's execution speed. It only stabilizes the reference. The function body runs at exactly the same speed with or without useCallback.

⚠️

Many developers think useCallback alone prevents child re-renders — but useCallback is only half the equation. The child component must also be wrapped in React.memo. Without React.memo, the child re-renders whenever the parent re-renders, regardless of whether the prop function reference changed.

⚠️

Many developers think adding useCallback to all functions is a performance best practice — but this is premature optimization and often makes things worse. useCallback adds comparison overhead on every render. For components with simple children or functions not in deps arrays, the cost exceeds the benefit.

⚠️

Many developers think useCallback with an empty dependency array creates a function that is always the same — it does create a stable reference, but the function body closes over the values from the first render only. If the function uses state or props without listing them as deps, it has a stale closure. The stable reference and stale values can create subtle bugs.

⚠️

Many developers think useCallback and useMemo are interchangeable for functions — useCallback(fn, deps) is literally sugar for useMemo(() => fn, deps). The only difference is intent: useCallback signals "I'm memoizing a function", useMemo signals "I'm memoizing a computed value".

⚠️

Many developers think inline arrow functions in JSX always cause performance issues — but for non-memoized children (which is most children), new function references make absolutely no difference. The child re-renders when the parent re-renders regardless. The function reference is irrelevant.

Where You'll See This in Real Code

Design system component libraries (like Radix UI and MUI) internally wrap all callback props in useCallback to prevent consumers' usage from accidentally invalidating memoization. When a Button component receives an onClick, the library ensures the internal event handler has a stable reference — consumers shouldn't need to think about this.

Virtual list implementations (react-window, react-virtual) require stable row renderer functions. Each row is a memoized component — if the renderRow function changes on every parent render, all thousand rows re-render on every scroll event. useCallback on the row renderer is non-negotiable for these use cases.

Real-time dashboards with frequent state updates use useCallback to prevent non-data components (navigation, filters, headers) from re-rendering when the live data updates. The filter handlers are wrapped in useCallback so the filter bar doesn't re-render when new data points arrive at 10 times per second.

Custom hooks that return functions always wrap them in useCallback so consumers can safely put them in useEffect dependency arrays. If useFetch returns a refetch function without useCallback, every consumer's useEffect would run on every render — this is why all well-written custom hooks return memoized functions.

Event delegation patterns in large lists (product catalogs, data tables) use useCallback for the event handlers to prevent list item re-renders. When a user opens a dropdown, only the state for that dropdown changes — all other items should skip re-rendering, which requires their onClick handlers to be stable.

Interview Cheat Sheet

  • useCallback(fn, deps) returns the same function reference until deps change
  • Does NOT make the function faster — only stabilizes its reference
  • Useful ONLY when: function is passed to React.memo child, OR function is in useEffect deps
  • Without React.memo on the child — useCallback is pure overhead with no benefit
  • useCallback(fn, deps) === useMemo(() => fn, deps) — identical under the hood
  • Stale callback: missing deps → function closes over old values → use functional setState or add to deps
  • Stable callback pattern: store fn in ref (useEffect) → expose stable useCallback wrapper
  • Empty deps []: stable reference but stale closure — function always sees first render's values
  • Custom hooks: always wrap returned functions in useCallback — consumers will put them in deps arrays
  • React Compiler (React 19): auto-memoizes callbacks — most manual useCallback calls become unnecessary
  • Cost: comparison + cache allocation on every render — negative ROI for simple/non-memoized children
💡

How to Answer in an Interview

  • 1.When asked "what is useCallback", lead with the reference stability explanation rather than performance: "useCallback returns the same function reference across renders. This matters when the function is passed to a React.memo child — without useCallback, the memo never helps because function references always differ." Most candidates say "it's for performance" without explaining the mechanism.
  • 2.The "is useCallback always beneficial" trap question separates mid from senior. Answer no: "useCallback adds comparison overhead on every render. It only helps when the prevented re-render or effect re-run cost exceeds the memoization cost. For simple non-memoized children, useCallback makes things slightly worse."
  • 3.The useCallback + React.memo relationship is asked as an implementation question at Flipkart and Atlassian: "Show me how to prevent a child component from re-rendering when an unrelated state in the parent changes." Write the parent with useState, wrap the child in React.memo, wrap the handler in useCallback. Explain why all three pieces are required.
  • 4.When discussing stale closures with useCallback, show the useRef escape hatch pattern: store the latest fn in a ref inside useEffect, expose a stable useCallback wrapper that calls ref.current. Then mention that React 19 formalizes this as useEffectEvent — which signals you know the ecosystem roadmap.
  • 5.Connect useCallback to custom hooks in interviews: "Every function I return from a custom hook is wrapped in useCallback. This is because I don't know how consumers will use it — they might put it in a useEffect dependency array. If the function reference changes on every render, their effect runs every render. Stable returns are a contract of a well-designed hook."

Practice Questions

No questions tagged to this topic yet.

Related Topics

useState Hook — Complete React Interview Guide
Beginner·4–8 Qs
useMemo Hook — Complete React Interview Guide
Intermediate·4–8 Qs
useEffect Hook — Complete React Interview Guide
Beginner·4–8 Qs
useRef Hook — Complete React Interview Guide
Beginner·4–8 Qs
🎯

Can you answer these under pressure?

Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.

Practice Free →Try Output Quiz