Table of contents
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 aboutImportance of using it
The benefits attached
How we can improve our code using Memoization
When we should use the React
useCallback
hookHow 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 usinguseCallback
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
: WhileuseCallback
is a powerful tool for improving performance, it can also make your code more complex and harder to understand. Make sure to useuseCallback
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
anduseMemo
: While bothuseCallback
anduseMemo
are used for memoization in React, they are used for different purposes.useCallback
memoizes a function, whileuseMemo
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.