React Hooks: Mastering useCallback

Tito AdeoyeTito Adeoye
5 min read

In React applications, performance optimization often comes down to controlling when and how components re-render. One common issue developers encounter is the unnecessary recreation of functions on every render, which can lead to inefficient rendering behavior—especially when passing callbacks to child components.

This is where useCallback comes in.

The useCallback hook provides a way to memoize functions, ensuring that a function instance remains stable between renders unless its dependencies change. If you've already read our article on useMemo, you're familiar with how memoization can improve performance by caching the results of expensive computations. In a similar vein, React’s useCallback hook helps optimize components—not by caching values, but by memoizing functions. In this article, we'll explore how useCallback works, when to use it, and when it might be better to avoid it.

What is useCallback?

The useCallback hook is a React hook that returns a memoized version of a callback function. This means that as long as its dependencies haven’t changed, React will return the same function instance across renders.

At first glance, it might look similar to useMemo, but while useMemo caches values, useCallback is specifically designed to memoize functions.

const memoizedCallback = useCallback(myFunction, [dependencies]);
  • myFunction: This is the function that you want to cache. useCallback returns this function and you can call this function this way: memoizedCallback(). On re-renders, React returns the same function instance as long as none of the specified dependencies have changed.

  • dependencies: this is a list of values outside the scope of your function that trigger the recreation of your function when they change - like props, state, or variables. If nothing changes in the dependencies, React knows the logic inside the function hasn’t changed either, so it safely reuses the previous function reference.

Why Avoid Recreating Functions?

In JavaScript, functions are objects. Every time you declare a function—whether as an inline arrow function or with the function keyword—you’re creating a new object in memory. Even if the code inside the function is identical to a previous one, the JavaScript engine considers it a new instance with a new reference.

In React, this matters. When a component re-renders, any functions declared inside it (including inline callbacks passed to child components) are recreated. That’s not necessarily a problem—unless those functions are being passed as props to children that rely on shallow comparison (like those wrapped in React.memo) or useEffect/useLayoutEffect hooks that depend on them.

Now reference equality matters. The key point is that React doesn't care what a function does, only whether it's the same. If a child component receives a new function reference on every render, React assumes it's a new prop—even if the function’s logic is unchanged. This breaks memoization and causes unnecessary re-renders.

Example:

const Parent = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log('Clicked');
  };

  return <Child onClick={handleClick} />;
};

Even though handleClick does the same thing every time, it's redefined on every render. If Child is memoized, it’ll still re-render because onClick is seen as a new prop.

Now compare with:

const handleClick = useCallback(() => {
  console.log('Clicked');
}, []);

With useCallback, handleClick stays the same object across renders, unless the dependencies change. This helps React skip unnecessary work—especially when scaling up to complex trees or high-frequency re-renders.

Garbage Collection & Memory Use

Every time a new function is created (i.e., on every render without useCallback), the old one becomes unreachable and is eventually collected by the JavaScript garbage collector. While modern engines are efficient at cleaning up, frequent function recreation can still result in short-lived memory churn, especially in highly dynamic components.

💡
Memory churn is the rapid and repeated allocation and deallocation of memory—usually involving short-lived objects that are created and discarded frequently.

In the context of JavaScript (and React):

  • Every time your component re-renders and creates a new function (instead of reusing one with useCallback), a new object is added to memory.

  • That old function is now unused and becomes a candidate for garbage collection.

  • The garbage collector eventually cleans it up—but doing this constantly, at high frequency, creates a cycle of allocation ➝ discard ➝ cleanup.

This cycle puts pressure on the JavaScript engine’s garbage collector, which has to work harder and more often. While modern engines are efficient, in apps with lots of re-renders (like high-FPS animations, data dashboards, etc.), this can add up and affect performance.

Interaction with useEffect / useLayoutEffect

One often-overlooked pain point is using functions as dependencies in useEffect. If you don’t use useCallback, the function reference changes on every render, which causes the effect to re-run every time. This can lead to logic bugs or unintended behavior.

useEffect(() => {
  doSomething();
}, [someFunction]); // this will fire on every render unless someFunction is memoized

By memoizing someFunction with useCallback, you prevent unnecessary effect executions.

✅ Rule of Thumb: When to Use useCallback

A subtle point worth mentioning for thoroughness: memoizing functions with useCallback has a cost. React needs to store the memoized function and check dependencies on each render. In low-frequency re-render scenarios, this cost may outweigh the benefits.

So while useCallback helps with optimization, it’s not always a net gain—and using it everywhere "just in case" can actually hurt performance.

Use useCallback when both of the following are true:

  1. You’re passing a callback to a child component that relies on referential equality to prevent unnecessary renders (e.g., it's wrapped in React.memo).

  2. The function is being recreated frequently, and that recreation is causing performance issues or unnecessary re-renders.

🔍 Quick Checklist

Ask yourself:

  • Is this function defined inside my component?

  • Am I passing it as a prop to a memoized child (React.memo, PureComponent, etc.)?

  • Does that child depend on reference equality to avoid re-renders?

  • Have I actually observed performance problems from this?

If you answered yes to most of these, useCallback is probably worth using.

Conclusion

Understanding useCallback isn’t just about knowing when to add it—it's about understanding why it exists. React re-renders, reference equality, and JavaScript's memory model all play a part. Now that you've seen the mechanics behind the scenes, you can make informed choices about when useCallback actually helps, and when it just adds noise. Like most tools in React, it's powerful when used with intention.

And as always—uh… euphoric engineering? No? We’ll work on it. Happy coding🎉!

0
Subscribe to my newsletter

Read articles from Tito Adeoye directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tito Adeoye
Tito Adeoye