Implement a polyfill for useCallback
`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.
useCallback is useMemo for functions — same memoization machinery, specialized to return a function reference.
Identity
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.
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
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) — withoutuseCallback, 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
const onClick = useCallback(() => {
console.log(count); // captured at this render
}, []); // empty deps → captures count=0 foreverFix: 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.