Back to React
React
easy
mid

How would you implement a polyfill for the useCallback hook?

`useCallback(fn, deps)` returns a stable function reference until `deps` change; equivalent to `useMemo(() => fn, deps)`. Polyfill via the same hook slot machinery: store [fn, prevDeps] across renders; if deps unchanged, return the previous fn; else store and return the new one.

4 min read·~20 min to think through

useCallback is useMemo for functions — same memoization machinery, specialized to return a function reference.

Identity

js
useCallback(fn, deps)   ≡   useMemo(() => fn, deps)

Both return the same value across renders iff the deps haven't changed (by Object.is comparison).

The polyfill

Hooks are implemented as an array of "slots" per component, advanced by a counter on each render. The component records its hooks in the same order every time.

js
let currentComponent = null;     // set by the renderer before each render

function renderComponent(component) {
  currentComponent = component;
  component.hookIndex = 0;
  const result = component.render();
  currentComponent = null;
  return result;
}

function useCallback(fn, deps) {
  const c = currentComponent;
  const i = c.hookIndex++;
  const prev = c.hooks[i];

  if (prev && depsEqual(prev.deps, deps)) {
    return prev.value;             // reuse previous fn
  }
  c.hooks[i] = { value: fn, deps };
  return fn;
}

function depsEqual(a, b) {
  if (!a || a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (!Object.is(a[i], b[i])) return false;
  }
  return true;
}

useMemo is the same engine

js
function useMemo(factory, deps) {
  const c = currentComponent;
  const i = c.hookIndex++;
  const prev = c.hooks[i];

  if (prev && depsEqual(prev.deps, deps)) {
    return prev.value;
  }
  const value = factory();
  c.hooks[i] = { value, deps };
  return value;
}

function useCallback(fn, deps) {
  return useMemo(() => fn, deps);
}

What's true about useCallback in real React

  • Always returns either the previous fn or the new fn — same identity model.
  • Deps compared by Object.is (referential for objects/arrays; value for primitives).
  • No deps array → returns a new fn every render (rare; usually a mistake).
  • Empty [] → returns the first render's fn forever (often captures stale values via closure).

When does it actually help?

  • The callback is passed to a memoized child (React.memo) — without useCallback, the child re-renders every time.
  • The callback is a dep of another effect / memo.
  • The callback is passed to a library that memoizes by identity (rare).

Otherwise it's noise — making a function "stable" that nothing depends on is pure ceremony. The React docs explicitly say this.

The stale-closure trap

jsx
const onClick = useCallback(() => {
  console.log(count);    // captured at this render
}, []);                  // empty deps → captures count=0 forever

Fix: include count in deps (returns a new fn when count changes), or use a ref for the latest value, or move the read inside via useReducer-style.

Interview framing

"Hooks are an ordered list of slots per component; on each render the component's hook index resets and advances per call. useCallback(fn, deps) reads the previous slot — if deps are equal by Object.is, return the previous fn; else store the new fn and deps and return it. It's identical to useMemo(() => fn, deps). The interview catch: useCallback only matters when something depends on referential stability — a memoized child, an effect dep array, or an external memoization library. Otherwise it's overhead with no benefit, and an empty deps array is a stale-closure invitation."

Follow-up questions

  • Why are useCallback and useMemo the same engine?
  • When does useCallback actually improve performance vs add overhead?
  • Why is `useCallback(fn, [])` often a stale-closure bug?
  • Why does Object.is matter for the deps comparison?

Common mistakes

  • Wrapping every function in useCallback unconditionally.
  • Empty deps + capturing changing state.
  • Forgetting deps when the callback closes over them.
  • Assuming useCallback prevents the parent from re-rendering — it doesn't.

Performance considerations

  • Each useCallback runs a tiny equality check + slot write. The benefit shows only when callees rely on referential stability; otherwise it's pure cost.

Edge cases

  • Custom equality not supported — comparison is always Object.is on deps.
  • Hook-order violation (conditional hook) breaks the slot index.
  • Stale closures in handlers attached to long-lived elements.

Real-world examples

  • Passing handlers to `React.memo` rows in a long list.
  • Stable callback for an effect's deps to avoid re-running.

Senior engineer discussion

Seniors use useCallback intentionally — only when a downstream consumer actually depends on identity. They reach for `useEvent`-style patterns or refs when the goal is 'always latest' rather than 'stable identity'.

Related questions