If you are not already familiar with the concept of hooks, be sure to check this article, as it provides a very good, in-depth overview of the concept of hooks, as well as some examples of them.

Introduction

The useCallback hook is used to cache a memoized callback function in order to save any recomputation overhead.

This hook is preventing a component from re-rendering unless its props have changed, meaning that we are now allowed to isolate resource-intensive functions so that they won’t automatically run on every component render.

It would be better to showcase a scenario where it would be beneficial to use the hook so we can get a better understanding of the steps we’ve taken to reach an issue, and then explain the thought process behind using the useCallback hook.

Project Overview

We’ll start by scaffolding a brand-new React project. First, we’ll create a new project directory, after which we’ll initialize a new project using the terminal.

You can use either npm, npx, or yarn for this process. The command you would be running is:

  • npm: npm init react-app app-name
  • npx: npx create-react-app app-name
  • yarn: yarn create react-app app-name

Now that we’ve got everything set up, let’s get right to the fun part.

Project Progression

Since this is a small project we’ll put all of the code inside the App.js file under the root src directory, which will look something like this:

import { useState, memo } from "react";
import './App.css';

const Todos = ({ todos, addTodo }) => {
  console.log("child render");

  return (
    <div className="todos-container">
      <h2>My Todos</h2>
      <div className="todos">
        {todos.map((todo, index) => {
          return <p key={index}>{todo}</p>;
        })}
      </div>
      <button onClick={addTodo}>Add Todo</button>
    </div>
  );
};

const MemoizedTodos = memo(Todos);

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);

  const increment = () => {
    setCount((c) => c + 1);
  };

  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    <div className="App">
      <MemoizedTodos
        todos={todos}
        addTodo={addTodo}
      />
      <hr />
      <div className="counter-container">
        <p>Count: {count}</p>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
};

export default App;

There’s quite a bit to unpack here, so let’s see what we have.

First, we have defined a Todos component which gets as props a list of todos, as well as for a function that, once called, will add a new todo. The component is tasked with rendering the todos to the screen, and also adding a new todo when the Add Todo button is pressed.

Then, we have the rest of the App component, which, apart from rendering and passing props to the MemoizedTodos component, also displays a counter which can be incremented to the screen.

And in order to make everything look a tad bit better, we’ll also add the following styles to our App.css file:

.App {
  text-align: left;
  width: 80vw;
  margin: 5vh auto 0 auto;
}

So, what’s the issue?

Now, you might be asking yourself: “what’s wrong with the application?”; indeed, everything seems to be working fine. The problem, however, is that every time we click the + button to increment the counter, the Todos component gets re-rerendered.

We can check that by inspecting the console, where we have been printing to the console each time we have re-rendered the Todos component:

Unexpected Todos component re-renders

The root cause of the issue

You see, within the App component we have defined the addTodo function, which will be recreated each the App component re-renders. The App component re-renders when its state changes, which consists of both todos, and the counter.

We are using memo, so the Todos component should not re-render since neither the todos state nor the addTodo function are changing when the count is incremented.

So, basically, the issue is that the addTodo is defined inside the App component, and gets recreated each time the component re-renders.

Solution

There are actually two ways we can solve this issue:

  • By defining the addTodo function outside the App component
  • By memoizing the addTodo function

While the first one is seemingly the clear solution, we won’t be able to always rely on excluding a function from the scope of a component by making it a pure function.

Memoizing the function

We’ll then go over the useCallback hook, which solves this exact problem we are facing right now by memoizing the function in order to avoid its reconstruction.

All that we have to do is to import the useCallback hook from React and assign the result of calling the hook with the hook’s callback returning the state update to the addTodo function as such:

const addTodo = useCallback(() => {
  setTodos((t) => [...t, "New Todo"]);
}, []);

And this slight adjustment should solve our previous issue.

Now, if you’re wondering why we haven’t added the todos array to the dependency array of the hook, that’s because React picks the state up since we’re using the setTodos function with the callback as the argument, rather than an array value.

If we were to change that we should see a warning:

Summary

I hope you have enjoyed reading this article and that you’ve got a better understanding of what the useCallback hook is, what it does, and also when you should be using it.

Feel free to leave a comment below in case you have any questions, inquiries, or any feedback to offer on this article.

See you on the next one. Cheers!

👋 Hey, I'm Vlad Mihet
I'm Vlad Mihet, a blogger & Full-Stack Engineer who loves teaching others and helping small businesses develop and improve their technical solutions & digital presence.

💬 Leave a comment

Your email address will not be published.

We will never share your email with anyone else.