How to effectively use useCallback in a React app

How to effectively use useCallback in a React app
* ThatSoftwareDude.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to Amazon.com. It really does help keep the servers running!

If you're building a React application then understanding how and when to use the useCallback hook can help prevent unnecessary re-renders and it can optimize your app’s performance.

In this article, we'll explore what useCallback is, how it works, and how to use it effectively in a React application.

And if you're relatively new to React, then I recommend the book React Key Concepts as a good starting point. 

What is useCallback in React?

useCallback is a hook introduced in React 16.8. It allows you to cache a function so that it doesn't get recreated every single time there is a render or re-render. This is particularly useful when you pass functions as props to child components or use them in dependency arrays.

By default, in React, every time a component re-renders, all of the functions inside that component are also recreated. In smaller applications, this isn't much of a problem an you won't notice much of a lag or performance hit. But in larger, more complex apps, unnecessary function recreation can lead to performance bottlenecks and degradation.

Syntax of useCallback

The useCallback hook has the following syntax:

const cachedCallback = useCallback(() => {
  // Your function logic here
}, [dependencies]);

The first argument to the hook is the function value that you want to cache. And the second resembles that of the useEffect hook and is comprised of an array of dependencies. The dependencies can include props, state and any variables and functions that are declared inside of the component body.

Essentially, if your function depends on any of these elements, then they should be added as dependencies.

On the first render, useCallback will return the function that is being passed in. While on subsequent renders, it will return the function that is already cached from a previous render.

If the dependencies have changed however, then React will return the function being passed during that render once again.

When should you use useCallback?

You should consider using useCallback in the following situations:

Passing functions as props: When you need to pass a function to a child component, using useCallback ensures that the function reference stays the same unless its dependencies change. This prevents unnecessary re-renders of the child component.

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('Child component rendered');
  return <button onClick={onClick}>Click Me</button>;
});

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

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
      <ChildComponent onClick={handleClick} />
    </div>
  );
};

export default ParentComponent; 

Event handlers: Functions used in event handlers (like onClick or onChange) that are wrapped with useCallback will only be recreated when necessary.

Optimizing heavy computations: If your callback function performs expensive computations, you can use useCallback to cache it, preventing unnecessary recalculations on each render. For this particular scenario, you will need to keep an eye on rendering and stay mindful if and when you see either a slowdown or a flicker.

Examples

Here is a quick example of how useCallback works:

import React, { useState, useCallback } from 'react';

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

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

In this example, the increment function is wrapped in the useCallback hook ensuring that it’s not recreated on every re-render. If the component needed to be re-rendered at some point in time, React would check to see if there is already a cached version of the increment function.

Another benefit of useCallback shines when you're working with child components. Passing a new function reference to a child component on every render can trigger unnecessary re-renders. Let’s see an example:

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('Child component rendered');
  return <button onClick={onClick}>Click Me</button>;
});

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

  const handleClick = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <h1>Count: {count}</h1>
      <ChildComponent onClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

If we didn't wrap the handleClick function in useCallback, React would recreate the function on every single render. Since a new function reference would be passed as a prop to ChildComponent, it would cause the child to re-render unnecessarily.

Wrapping the handleClick function in useCallback ensures that it only gets recreated when the count state variable changes.

Common pitfalls to avoid

Overall useCallback can be a vey useful tool when it comes to optimization, but it’s important to use it wisely. Overusing it or using it inappropriately can lead to more complexity without significant performance gains. Here are a few common mistakes to watch out for:

Overusing useCallback: Try to avoid wrapping every function in useCallback as it is unnecessary and could lead to confusing code. Only use it when you have performance issues related to unnecessary re-renders.

Ignoring dependencies: Always make sure you specify the correct dependency list. If you forget to include dependencies that the callback depends on, the function might not behave as expected.

Relying on useCallback too much: Not all performance issues can be solved with useCallback. Sometimes, problems arise due to improper state management or other factors and caching functions isn't going to lead to any real improvements.

useCallback vs. useMemo

Sometimes, developers confuse useCallback with useMemo. Both are React hooks used for optimization, but they serve different purposes:

useCallback caches a function to avoid unnecessary recreation. Whereas useMemo caches a computed value (e.g., the result of an expensive calculation) to avoid unnecessary recalculations.

Here's a quick comparison example:

const memoizedValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

const memoizedCallback = useCallback(() => {
  setState((prevState) => prevState + 1);
}, []);

In this case, useMemo is used to cache the return value of computeExpensiveValue, while useCallback is used to cache a function.

Both equally important optimization methods, but with slightly different purposes.

Conclusion

Using useCallback is a great way to optimize your React app’s performance by preventing unnecessary function recreations and re-renders. It's especially useful when passing functions as props or using event handlers in large-scale applications.

However, remember that useCallback is not a one-size-fits-all solution. It’s essential to understand when and where it’s appropriate to use it for actual performance gains. Overuse can complicate your code without providing much benefit.

Walter G. author of blog post
Walter Guevara is a Computer Scientist, software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20 years.

Get the latest programming news directly in your inbox!

Have a question on this article?

You can leave me a question on this particular article (or any other really).

Ask a question

Community Comments

No comments posted yet

Add a comment