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:
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 theApp
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!
💬 Leave a comment