Why you should use React useCallback Hook: A Deep Dive

On the issue of component re-renders, while building React apps, we will be taking a deep dive into using one of the React state hooks called useCallback.

We'll be looking at

  • What useCallback is about

  • Importance of using it

  • The benefits attached

  • How we can improve our code using Memoization

  • When we should use the React useCallback hook

  • How to use it

  • Common mistakes to avoid

  • How to know what dependency(ies) to include in the dependency array

Consider a scenario where you have a component that displays a list of items. Each item has a button that increments its count when clicked. The component re-renders every time the count of any item changes.

import React, { useState } from 'react';

const Item = ({ count, onClick }) => {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={onClick}>Increment</button>
    </div>
  );
};

const ItemList = () => {
  const [items, setItems] = useState([
    { id: 1, count: 0 },
    { id: 2, count: 0 },
    { id: 3, count: 0 },
  ]);

  const handleClick = id => {
    setItems(prevItems =>
      prevItems.map(item => {
        if (item.id === id) {
          return { ...item, count: item.count + 1 };
        }
        return item;
      })
    );
  };

  return (
    <div>
      {items.map(item => (
        <Item key={item.id} count={item.count} onClick={() => handleClick(item.id)} />
      ))}
    </div>
  );
};

export default ItemList;

In this example, the handleClick function takes an id as a parameter, which represents the id of the item whose count is being incremented. The setItems state updater is used to update the items state, changing the count of the item with the matching id. The Item component takes count and onClick props and displays the count and a button. The ItemList component maps over the items state and passes each item's count and handleClick to an instance of the Item component.
One of the possible ways of solving this issue is with the use of useCallback. So what is useCallback and it's importance in React? Let's dive into it.

useCallback and its importance

useCallback is a React Hook that allows you to memoize a function. It is used to optimize the performance of your React components by avoiding unnecessary re-renders.

The syntax for the useCallback Hook is as follows:

const memoizedCallback = useCallback(
  () => {
    // Your function logic here
  },
  [dependency1, dependency2, ...], // List of dependencies
);

In the code above, memoizedCallback is a variable that holds the memoized version of the function being passed to useCallback. The first argument to useCallback is the function that you want to memoize. The second argument is an array of dependencies, which are values that your function depends on. When any of these dependencies change, the function will be recreated.

For example, if your function depends on a count value, you would pass [count] as the second argument to useCallback:

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

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

In this case, the memoizedCallback will be recreated only when the count value changes.

The importance of useCallback lies in the fact that it helps to reduce the number of re-renders in your components, thereby improving the performance of your application. In React, components re-render every time their state or props change. If a component re-renders, all its children components also re-render. In some cases, this can cause your application to become slow, especially when you have large and complex components.

By memoizing a function using useCallback, you ensure that the function is only re-created when it needs to be, avoiding unnecessary re-renders and improving the performance of your application. This can be especially useful in scenarios where you have complex components that perform expensive computations or where you have components that render frequently, such as in lists or tables. Let's see what benefits are attached to using React useCallback Hook in the next section of our article.

Benefits of using React useCallback

The benefits of using useCallback in React are:

  • Improved performance: By memoizing a function using useCallback, you avoid unnecessary re-renders, which can improve the performance of your application.

  • A better understanding of component behavior: By using useCallback, you make it clear which functions depend on which values, making it easier to understand the behavior of your components.

  • Reduced complexity: In some cases, useCallback can reduce the complexity of your code by avoiding the need for additional logic to control when functions are re-created.

  • Increased code reusability: By memoizing functions, you can make them more reusable across different components.

  • Better debugging: When a function is memoized, it will be the same instance across renders, making it easier to debug and understand its behavior.

Overall, using useCallback can make your code more efficient, easier to understand, and easier to maintain, making it an essential tool in any React developer's toolkit.
So, how can we improve our codebase by using memoization through useCallback?

How to improve our code using Memoization?

Memoization is a technique for improving the performance of a function by caching its results so that, if the function is called again with the same arguments, the cached result can be returned instead of re-computing the result. This can lead to substantial performance improvements in cases where a function is called repeatedly with the same arguments.

In React, memoization can be achieved using useCallback or other libraries that provide memoization functionality. To improve performance through memoization in React, you should focus on memoizing functions that are called frequently and/or are expensive to compute.

Going back to our example earlier in the article, here's how the component could be written using useCallback:

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

const Item = ({ count, onClick }) => {
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={onClick}>Increment</button>
    </div>
  );
};

const ItemList = () => {
  const [items, setItems] = useState([
    { id: 1, count: 0 },
    { id: 2, count: 0 },
    { id: 3, count: 0 },
  ]);

  const handleClick = useCallback(id => {
    setItems(prevItems =>
      prevItems.map(item => {
        if (item.id === id) {
          return { ...item, count: item.count + 1 };
        }
        return item;
      })
    );
  }, []);

  return (
    <div>
      {items.map(item => (
        <Item key={item.id} count={item.count} onClick={() => handleClick(item.id)} />
      ))}
    </div>
  );
};

export default ItemList;

In this example, the handleClick function is created using useCallback and its dependencies are specified as an empty array, meaning it won't change during the lifetime of the component. As a result, the handleClick function will only be created once, and the same instance will be used on every render. This avoids unnecessary re-renders of the component and improves performance. The next section will be on when we are expected to make use of React useCallback.

When should we make use of React useCallback Hook?

Here are a few scenarios where you can use useCallback in React:

  • When passing a function as a prop to a child component: If the child component re-renders frequently, memoizing the function using useCallback can help avoid unnecessary re-renders and improve performance.

  • When using a callback in a performance-critical section of code: For example, if you're using a callback in a useEffect hook to update the state, memoizing the callback using useCallback can prevent unnecessary re-renders and improve performance.

  • When handling events: If you're passing a callback function to handle events in your component, you should consider memoizing the callback using useCallback to prevent unnecessary re-renders.

  • When fetching data: If you're using a hook-like useEffect to fetch data, memoizing the callback function used to fetch the data can prevent unnecessary re-renders and improve performance.

  • When creating a custom hook: If you're creating a custom hook that relies on a callback, you should consider memoizing the callback using useCallback to prevent unnecessary re-renders and improve performance.

These are just a few examples of when you might want to use useCallback in your React components. The key is to understand when a component re-renders unnecessarily, and when memoizing a callback can help avoid those re-renders and improve performance.

In our next section, we'll be looking at simple and advanced examples of the usage of useCallback.

How to use React useCallback Hook

Here's a simple example of using useCallback in React:

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

function Child({ onClick }) {
  return (
    <div>
      <button onClick={onClick}>Click me!</button>
    </div>
  );
}

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

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

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

In this example, we have a Parent component that contains a Child component. The Parent component keeps track of a count state, and the Child component has a button that when clicked, increments the count.

The Parent component passes a callback onClick to the Child component, which is used to handle the button click. The callback is memoized using useCallback so it will only be re-created when its dependencies change (in this case, there are no dependencies, so it will only be created once).

By memoizing the callback, we can prevent the Child component from re-rendering unnecessarily when the Parent component re-renders, which can improve performance.

Here's an example of advanced usage of useCallback in React:

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

function Child({ onClick, count }) {
  return (
    <div>
      <button onClick={onClick}>Click me!</button>
      <p>Child count: {count}</p>
    </div>
  );
}

function Parent() {
  const [parentCount, setParentCount] = useState(0);
  const [childCount, setChildCount] = useState(0);

  const handleParentClick = useCallback(() => {
    setParentCount(prevCount => prevCount + 1);
  }, []);

  const handleChildClick = useCallback(() => {
    setChildCount(prevCount => prevCount + 1);
  }, [parentCount]);

  return (
    <div>
      <p>Parent count: {parentCount}</p>
      <button onClick={handleParentClick}>Increment parent count</button>
      <Child onClick={handleChildClick} count={childCount} />
    </div>
  );
}

In this example, we have a Parent component that contains a Child component. Both the Parent and the Child components keep track of their own count state.

The Parent component has a button that when clicked, increments its own count. The Child component has a button that when clicked, increments its own count.

The Parent component passes two callbacks to the Child component: one to handle clicks on the parent button, and another to handle clicks on the child button.

The handleParentClick callback is memoized using useCallback with an empty dependency array, so it will only be re-created once. The handleChildClick callback is memoized using useCallback with parentCount as a dependency, so it will be re-created whenever the parentCount changes.

By memoizing the handleChildClick callback in this way, we ensure that the Child component will re-render only when its dependencies change, which can improve performance.

Common mistakes to avoid when using useCallback

Here are some common mistakes to avoid when using useCallback in React:

  • Not including all necessary dependencies: If you forget to include a dependency in the dependency array of useCallback, it can cause your callback to be re-created unnecessarily, which can lead to performance issues.

  • Overusing useCallback: While useCallback is a powerful tool for improving performance, it can also make your code more complex and harder to understand. Make sure to use useCallback only when it's necessary, and avoid using it just for the sake of using it.

  • Not understanding the trade-off: useCallback can improve performance by avoiding unnecessary re-renders, but it also increases the memory usage of your application. Make sure to understand the trade-off and choose the right solution for your specific use case.

  • Not memoizing functions that are used in multiple places: If a function is used in multiple places, memoizing it with useCallback can help prevent unnecessary re-renders. However, if you forget to memoize the function, it can cause unnecessary re-renders and decreased performance.

  • Not understanding the difference between useCallback and useMemo: While both useCallback and useMemo are used for memoization in React, they are used for different purposes. useCallback memoizes a function, while useMemo memoizes a value. Make sure to choose the right tool for the job.

The first point begs the question, "How do one determine the right and necessary dependency(ies) to include in the useCallback dependency array?" Okay, we move to tackle that very question.

How to know the necessary or right dependency(ies)

To determine what dependencies to include in the dependency array of useCallback, you need to understand what values are used inside the callback.

The dependencies you include in the array should be any values that the callback function uses, either directly or indirectly. This way, if any of these values change, useCallback will return a new function, causing a re-render.

Here's a simple example to help illustrate this concept:

const [count, setCount] = useState(0);
const [name, setName] = useState("John");

const handleClick = useCallback(() => {
  console.log(`Count: ${count}`);
  console.log(`Name: ${name}`);
  setCount(prevCount => prevCount + 1);
}, [count, name]);

In this example, the handleClick function uses both count and name directly. So, we include both values in the dependency array of useCallback.

If handleClick only used count, for example, we would only include count in the dependency array.

It's important to be mindful of the dependencies you include in the array, as adding too many can lead to unnecessary re-renders, and leaving out necessary dependencies can cause bugs. The goal is to include only the dependencies that are actually needed for the callback to work as intended.

In conclusion, the whole aim of this article is to spread the good news about the benefits of using React useCallback Hook in making our app perform better by reducing the number of re-renderings that will occur as it makes it easier to understand your components, assist in debugging errors, and lastly increases code reusability. Here's a recommendation for further reading on useCallback and furthermore on how to improve your code performance better.

I hope you enjoyed the ride so far. I'm hoping I was able to pass on some information on things you may not have known.

Check out my other articles and please subscribe to my newsletter for more updates on my blog. I'll like to connect with you as we learn together on Twitter and follow me on Hashnode. Thanks for reading.