If you’ve been working with React and performance optimization, you’ve probably heard of React.memo, useMemo, and useCallback.
Most tutorials show you “surface-level” examples, but today we’ll go deeper into useCallback — how JavaScript handles function references, why they cause unnecessary re-renders, and how React solves it under the hood.
Let’s get started. 🚀
đź§ The Root Problem: Functions Are Objects
In JavaScript, functions are objects.
That means every time you declare a function inside a component, a brand-new function object is created in memory on every render.
Even if two functions look the same, they don’t share the same reference.
👉 This becomes a problem when passing functions as props.
đź”´ Without useCallback: Function References Break Memoization
👉 What happens here?
- Parent re-renders whenever count changes.
- Each render creates a new function reference for handleClick.
- React.memo in Child compares props → sees onClick as changed (new reference).
- Child re-renders unnecessarily
🟢 With useCallback: Stable Function Reference
Now the child doesn’t re-render unless it needs to. 🎉
⚡ How useCallback Works Internally
const fn = useCallback(callback, deps);
- React does this internally:
- Checks if there’s a previous function stored in the hook state.
- Compares the new deps array with the old one (shallow compare)
- If deps are the same → reuse the old function reference.
- If deps changed → create and store a new function reference.
function useCallback(fn, deps) {
const last = getHookState(); // { fn, deps }
if (last && shallowEqual(last.deps, deps)) {
return last.fn; // reuse reference
}
setHookState({ fn, deps });
return fn; // new reference
}
So useCallback is basically a function reference cache.
🎯 The Mystery of the Empty Array []
When you write:
const fn = useCallback(() => {
console.log("Hello");
}, []);
- [] → no dependencies.
- React creates the function once and always returns the same reference.
- Perfect for callbacks that don’t depend on any state/props.
But ⚠️ watch out for the stale closure trap.
🕳️ The Closure Trap
const [count, setCount] = useState(0);
const logCount = useCallback(() => {
console.log(count);
}, []); // empty deps
- First render: count = 0.
- Function is created with count = 0.
- Even when count updates → function still closes over old value.
So logs will always show 0.
âś… Fix by adding dependencies:
const logCount = useCallback(() => {
console.log(count);
}, [count]); // updates with count
Now, the function is recreated whenever count changes.
⚖️ When to Use useCallback
âś… When passing functions to React.memo child components.
âś… When functions are dependencies of other hooks (useEffect, useMemo).
❌ Don’t wrap every function blindly → unnecessary overhead.
🚀 Final Recap
- Functions in JS are objects → new reference each render.
- React compares references, not contents.
- useCallback stabilizes function references until dependencies change.
- [] = never change (but beware stale closures).
- Works best with React.memo to prevent child re-renders.
🔗 If you’ve mastered useMemo and React.memo, useCallback is the missing piece that completes the optimization trio.
👉 Next time your child keeps re-rendering even though “nothing changed” — check if you forgot useCallback.
Top comments (0)