DEV Community

Vivek Lagshetti
Vivek Lagshetti

Posted on

🔥 Deep Dive into useCallback: Why Functions Break Memoization & How React Fixes It

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.

Functions are Objects in Javascript

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

Without useCallback hook Code snippet

👉 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

Without useCallback hook

🟢 With useCallback: Stable Function Reference

With useCallback hook

Now the child doesn’t re-render unless it needs to. 🎉

With useCallback

⚡ How useCallback Works Internally

const fn = useCallback(callback, deps);
Enter fullscreen mode Exit fullscreen mode
  1. React does this internally:
  2. Checks if there’s a previous function stored in the hook state.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

So useCallback is basically a function reference cache.

🎯 The Mystery of the Empty Array []

When you write:

const fn = useCallback(() => {
  console.log("Hello");
}, []);
Enter fullscreen mode Exit fullscreen mode
  • [] → 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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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)