Back to React
React
medium
mid

How do state and refs behave inside a useCallback hook?

useCallback memoizes a function so its reference is stable across renders — unless a dependency changes. Closures inside it capture values from the render where it was created; stale deps mean stale values. The function identity matters for child memoization and effect deps.

4 min read·~7 min to think through

useCallback(fn, deps) returns the same function reference between renders as long as deps are unchanged. The behaviors interviewers probe:

1. Closures capture the render's values

The function you pass closes over variables from the render where it was created. If a value isn't in deps, the callback keeps seeing the stale version:

jsx
const [count, setCount] = useState(0);

const log = useCallback(() => {
  console.log(count); // captures count from the render this was created in
}, []); // ❌ empty deps → always logs 0

Add count to deps and the callback is recreated when count changes, capturing the fresh value. Stale-closure bugs are the #1 thing being tested here.

2. The functional updater escape hatch

If you only need the latest state, not to render on it, use the updater form — then you can keep deps empty safely:

jsx
const increment = useCallback(() => {
  setCount((c) => c + 1); // no dependency on count
}, []);

3. Reference stability — why it exists

The point of useCallback isn't speed of the function — it's a stable reference:

  • Passed to a React.memo child as a prop → child doesn't re-render needlessly.
  • Used in another hook's dependency array (useEffect, useMemo) → effect doesn't re-fire every render.

Without useCallback, a new function is created every render — a new reference — defeating memo and retriggering effects.

4. It does nothing if deps change every render

jsx
const handler = useCallback(() => {...}, [{}]); // new object each render → never stable

If a dep is itself unstable, useCallback is pointless overhead.

5. It's a hint, not a guarantee

React may discard the memoized function. Don't rely on identity for correctness — only for optimization.

The framing

"useCallback gives you a stable function reference until deps change. Two things to watch: closures capture values from the creation render, so missing deps cause stale-closure bugs — fix with correct deps or the functional updater. And it only earns its keep when the reference actually matters: a memo'd child prop or another hook's dep array. Wrapping every function in it is cargo-culting."

Follow-up questions

  • What's the difference between useCallback and useMemo?
  • When does useCallback actually improve performance?
  • How does the functional updater form avoid a dependency?
  • Why might wrapping everything in useCallback be harmful?

Common mistakes

  • Empty deps with a callback that reads state/props — classic stale closure.
  • Wrapping every function in useCallback without a memo'd consumer.
  • Putting an unstable object/array in deps, making memoization useless.
  • Relying on reference identity for correctness, not just optimization.

Performance considerations

  • useCallback itself has a cost — it stores the function and compares deps every render. It only pays off when the stable reference prevents a more expensive re-render or effect. Used everywhere blindly, it's net-negative.

Edge cases

  • A dependency that's a new object literal every render.
  • Callback used in a useEffect dep array causing an effect loop.
  • React discarding the memoized callback (allowed by spec).

Real-world examples

  • A callback passed to a memoized list-row component to prevent re-rendering thousands of rows.
  • A fetch function used as a useEffect dependency, stabilized so the effect runs once.

Senior engineer discussion

Seniors explain useCallback as a reference-stability tool, not a speed tool, and immediately bring up stale closures and the functional-updater escape hatch. They stress it only helps when paired with React.memo or hook dep arrays, and that over-application adds overhead.

Related questions