React hooks, a feature introduced in version 16.8, helps developers code functional components. It makes writing functional components more appealing.
React has several Hooks spanning various categories — state, context, ref, effect, performance and others. Amongst all these,useCallback
can improve the performance of React apps. This hook is part of a family of hooks known as performance hooks, which include useMemo
and memo
.
In this article, we’ll dive into the useCallback
hook, its purpose, benefits, and the best cases for its application.
Prerequisites
To get the best from this article, you should have the following:
- Fundamental understanding of React, JavaScript, and hooks.
- Basic understanding of the
useMemo
hook – though I will still discuss it in this article. - Google Chrome Browser.
What is the useCallback hook?
The useCallback
hook is a built-in hook in React that lets you memoize a callback function by preventing it from being recreated on every render. In simple terms, it means that the callback function is cached and does not get redefined on every render. This will optimize and improve the overall performance of your application.
When you define a function inside a component, it is recreated on every render, even if the component’s state or props have not changed. This can lead to unnecessary re-renders, which can slow down your application’s performance. The useCallback hook helps you avoid this problem by memoizing the function and only recreating it when necessary.
To understand the useCallback
hook better, it’s important to grasp the concept of memoization. Memoization is an optimization technique that stores results from a computational event (like a function) in the cache, and on subsequent calls, it fetches the results directly from the cache without recomputing the result. This technique is very useful when dealing with expensive (time-consuming) computations or heavy data processing.
Memoization can be compared to a real-world scenario where you ask someone to calculate the result of 2 multiplied by 200. After a while, you randomly ask the same question again. You naturally expect that person to give you the answer immediately because they have already done the calculation before. This time-saving concept is similar to memoization, where previously computed results are stored and reused when needed.
The useCallback syntax
This hook follows a very simple pattern for utilization. It takes two arguments: the function you want to memoize, and the dependencies array.
useCallback(function, dependencies)
const updatedFunction = useCallback(
(args) => {
//action goes in here
},
[dependencies]
);
- The first argument is the function you want to memoize.
- The second argument is an array of dependencies. The elements in this array are the values on which the function to be memoized depends. If any of these values change, the function will be recreated.
Note, if you omit the dependencies array, the function will be re-defined on every render.
How to Memoize a React Component
In React, we implement memoization via React.memo()
, which is a higher-order component. The React.memo
serves as a wrapper for a component and returns a memoized output of that component, which prevents the component or sub-components from unnecessary re-rendering.
There are two ways by which we can use React.memo
in our component. We can either use it to wrap the entire component or add it to the part where we export the component.
In the example below you will find the first way of using it:
const newComponent = React.memo((props) => {
return (
//render with props
);
});
export default newComponent;
In the syntax above, the newComponent
component is wrapped with React.memo()
, which creates a memoized version of the component. This memoized version of the component will only re-render if the props passed to it have changed.
And here is the second way you can use React.memo
:
const newComponent = (props) => {
//render with props
}
export default React.memo(newComponent);
The syntax above denotes that we can memoize a component by simply passing it as an argument to React.memo
and exporting the result.
Note: React.memo
has nothing to do with React hooks. It is an in-built method in React used to aid the optimization of our React applications. If you prefer using a hook to memoize your component, you can use memo
in place of React.memo
.
When to use the useCallback hook
Now you understand how the useCallback
hook can optimize your app, let’s see some use cases:
- When you need to pass a function as props to a child component.
- If you have a function that is expensive to compute and you need to call it in multiple places.
- When dealing with functional components.
- When you are working with a function that relies on external data or state.
Note: Given the scenarios highlighted above, it’s still important to weigh the benefits and drawbacks of the hook and use it judiciously only where needed.
The difference between useCallback and useMemo
According to the React documentation, there is a hook called useMemo
which also falls under the performance hooks category. Despite being under the same category of hooks division, these hooks differ in purpose and usage.
Let’s look at the useMemo
syntax before discussing how it differs from useCallback
.
Just like useCallback
, this hook also takes two arguments – a function and the dependencies array.
const updatedValue = useMemo((args) => {
//function body that returns the value to memoize
},
[dependencies]
);
The major difference between the hooks is that the useCallback
returns a memoized function while the useMemo
returns a memoized value. It means that useMemo
can help prevent unnecessary computations as it caches the computed value of the function and useCallback
can help prevent unnecessary re-renders as it returns the memoized function that can be passed as props to the children’s components.
The useCallback
hook is ideal for functions that take a long time to compute or depend on external resources. In contrast, useMemo
is ideal for values that take a long time to compute or depend on external resources.
We can verify how each method works by running the following code in Playcode.io. We can call useMemo
inside App.jsx
to see the outputs.
import React, {useMemo, useState} from 'react';
const addition = (counter) => {
let newValue = counter;
for (let n = 0; n <= 10; n++) {
newValue += 1;
}
return newValue;
}export function App(props) {
const [initial, setInitial] = useState(0); const result = useMemo(()=> {
return addition(initial);
}, [initial])
console.log("useMemo:", result )
return (
<div className='App'>
<h1>Hello this is the {result}</h1>
</div>
);
}
When using useMemo
the console prints: “useMemo: 11”
Let’s now test what useCallback
results to in the same scenario:
import React, {useCallback, useState} from 'react';
export function App(props) {
const [count, setCount] = useState(0); // Define a callback function using useCallback
const handleClick = useCallback(() => {
return count;
}, [count]); console.log("useCallback:", handleClick ) return (
<div className='App'>
<p>Count: {count}</p>
</div>
);
}
In this example, the console prints a function “useCallback: f()”. When using this function in a properly-bootstrapped React project, the returned function is ( ) => { return count }
.
Benefits of using the useCallback hook
There are several advantages attached to using the useCallback
hook. Here are a few:
- Performance optimization: This hook optimizes the performance of your application by preventing a series of unnecessary re-rendering in your components.
- Restricting rendering of child components: The useCallback hook in React allows us to selectively render important child components in a parent component. By using the
useCallback
hook, we can create memoized functions and pass them as props to child components. This ensures that only the necessary child components are rendered and updated when specific actions occur, resulting in improved performance. - Preventing memory leaks: Since the hook returns the memoized function, it prevents recreating functions, which can lead to memory leaks.
Drawbacks of the useCallback hook
Before making use of this hook, take into consideration its challenges, then weigh if it is still important to apply it in a particular case. The drawbacks:
- Complex code: While this hook can help you create memoized functions, it can as well make your code complex. You must strike a balance between the usage of the hook and the complexity it adds to your code. Hence, only use the hook only when you need to memoize an expensive function which needs to be passed down to children components as a prop.
- Excessive memory usage: If you do not use the
useCallbck
hook properly, it can lead to excessive memory usage. For instance, if a memoized function holds onto references to objects or variables that are no longer needed, those resources may not be freed up by garbage collection and could use more memory than needed.
A practical example of the useCallback hook
In this section, you will see how to use the hook. We will see how React.memo
falls short in a Single Page Application (SPA) and the need for the useCallback
hook.
In this simple application, we have a Parent component with the name Parent.jsx
and three children components, namely Button.jsx
, Title.jsx
, and Display.jsx
which all rely on props from the Parent component.
Bootstrap a new React Project using Vite
Let’s bootstrap a new project with Vite:
npm create vite@latest useCallback-hook --template react
Clean up the default folder structure
First, try to clean up the folder structure by removing styles and the asset folder. The purpose of this article is to explain the useCallback
hook so I won’t be considering styling the web app.
Next, create a component
folder inside the src
folder. In the component
folder, create four files – Parent.jsx
, Title.jsx
, Button.jsx
, and Display.jsx
.
The file structure:
src
└── component
├── Parent.jsx
├── Title.jsx
├── Button.jsx
└── Display.jsx
The Parent.jsx
content:
// Parent.jsx
import React, { useState } from "react";
import Title from "./Title";
import Button from "./Button";
import Display from "./Display";const Parent = () => {
const [salary, setSalary] = useState(2000);
const [age, setAge] = useState(30); const incrementAge = () => {
setAge(age + 5);
}; const incrementSalary = () => {
setSalary(salary + 100);
};
return (
<div>
<Title />
<Display text="age" displayvalue={age} />
<Button handleClick={incrementAge}>Update Age</Button>
<Display text="salary" displayvalue={salary} />
<Button handleClick={incrementSalary}>Update Salary</Button>
</div>
);
};export default Parent;
The content of Title.jsx
are as follows:
// Title.jsx
import React from "react";const Title = () => {
console.log("Title Component is rendered");
return (
<h1>useCallback Hook.</h1>
);
};export default Title;
The Display.jsx
file contains the following:
// Display.jsx
import React from "react";const Display = ({ text, displayvalue }) => {
console.log("Display Component Rendered ", { displayvalue }); return (
<p>
This person's {text} is {displayvalue}
</p>
);
};export default Display;
Then, Button.jsx
is as follows:
// Button.jsx
import React from "react";const Button = ({ handleClick, children }) => {
console.log("Button Component Renders - ", { children });
return <button onClick={handleClick}>{children}</button>;
};export default Button;
Finally, App.jsx
contains these lines:
import Parent from "./components/Parent";
function App() {
return (
<>
<Parent />
</>
);
}export default App;
Launch the project on your browser by running these commands:
$ npm install
$ npm run dev
Here is the User Interface (UI):
Let’s break down the UI:
- Title: which says “useCallback hook”.
- Display: renders the person’s age and salary.
- Button: increments the age or the salary.
To get how performance works in the web application, I have added a console log statement to all the children’s components. The console log statements will help you visualize which component renders when you trigger an action.
By default, all the components render when you first launch the web application and you can confirm that by checking the logs in the console.
In the snapshot above, all five components are rendered. Recall that there are just three children components but five instances — where the Title
component is instantiated once, while Display
and Button
are instantiated twice inside the parent.jsx
file.
Next, clear the console and click the “Update Age” button, which causes a re-render for every component. You can confirm this by reading the console.
When you click on the “Update Salary Button”, all components re-render. This is not ideal, as it can lead to performance issues. For example, if we have a very large application, interaction with even a small part of the app will cause all components to re-render, and the application may take a long time to load, resulting in poor user experience.
The question now is how to optimize this simple application so that when you click the “Update Age” or “Update Salary” buttons, the Title
and Age
components do not re-render.
Memoizing the Sample Project using React.memo()
To memoize the sample application, wrap the children’s components with React.memo
.
After making the change, the new components should look like this:
The Title.jsx
file:
// Title.jsx
import React from "react";const Title = () => {
console.log("Title Component is rendered");
return (
<h1>useCallback Hook.</h1>
);
};export default React.memo(Title);
The Button.jsx
file looks like this:
// Button.jsx
import React from "react";const Button = ({ handleClick, children }) => {
console.log("Button Component Renders - ", { children });
return <button onClick={handleClick}>{children}</button>;
};export default React.memo(Button);
And the Display.jsx
component:
// Display.jsx
import React from "react";const Display = ({ text, displayvalue }) => {
console.log("Display Component Rendered ", { displayvalue }); return (
<p>
This person's {text} is {displayvalue}
</p>
);
};export default React.memo(Display);
Let’s test if React.memo
solved the issue we first encountered. Back in the browser, clear the console and click the buttons.
You get fewer re-renders, but we’re not done yet: the “Update Salary” button should not re-render when you click “Update Age`. The same thing can be said when you click the “Update Salary” button.
The extra re-render is caused because these functions in the Parent.jsx
file are recreated on every re-render:
const incrementAge = () => {
setAge(age + 5);
};
const incrementSalary = () => {
setSalary(salary + 100);
};
When dealing with functions, you need to consider referential equality: Two functions having the same behaviour do not make them equal to each other.
In our example case, the version of the functions before the components re-renders are different from the version of the functions after the components render.
Referential Equality
When dealing with equality in JavaScript, there are three methods by which this can be done:
==
is the loose equality operator.===
is the strict equality operator.object.is()
is similar to the strict equality operator. It checks if two values are identical, without any type coercion.
In React, when a component re-renders, it triggers a process known as reconciliation. The reconciliation process takes place within the Virtual DOM, a virtual representation of the actual DOM. It compares the previous state of the component with its new state, as well as the previous props with the new props.
To perform this comparison accurately, React employs a technique called the diffing algorithm. This algorithm utilizes the Object.is()
method to compare values without any type coercion, enabling React to precisely detect any actual changes between the previous and new states/props within the Virtual DOM.
By pinpointing the differences, React can apply targeted updates to the real DOM only where necessary, minimizing unnecessary manipulations and enhancing performance. This combination of the diffing algorithm, reconciliation within the Virtual DOM, and the Object.is()
method helps React optimize rendering and keep the user interface in sync with the component’s latest state.
In JavaScript, we treat functions as first-class citizens, which means that we can use them just like any other value or object. This treatment includes assigning them to variables, passing them as arguments to other functions, and returning them from functions. JavaScript considers functions as objects because they can be assigned to variables making them easily referenced in the memory.
Also, in JavaScript, objects, no matter how close or related, are not equal to each other because they have different references in the memory. This explains why the function before a component is rendered is different from the function after since functions are seen as objects.
In order to grasp the concept behind referential equality, navigate to your console tab then define two variables and assign primitive data types to them, then compare them using the equality operator. You will get the boolean value of true
returned but if you do this for things like arrays and objects the boolean value false
is returned. If you perform this operation in your console, you will get this:
Why did React.memo not fully memoize the application?
Having understood what referential equality is, we can now understand what the problem is. React.memo
does not prevent re-rendering in the children components because the props passed to the children components are functions.
Recall that the functions before and after component rendering are not the same. Here, React.memo
noticed that the props which are functions coming from the Parent component have changed, so it doesn’t stop the components from re-rendering.
The big question is how to let React know that the props have not changed so re-rendering is not triggered.
The solution to that is the useCallback
hook which will fully memoize our application.
Integrating useCallback into the application.
Firstly, import the useCallback hook at the top inside the component where you need it (Parent.jsx
).
import { useCallback, useState } from "react";
Next, rewrite the functions you want to memoize using the useCallback hook.
const incrementAge = useCallback(() => {
setAge(age + 5);
}, [age]);
const incrementSalary = useCallback(() => {
setSalary(salary + 100);
}, [salary]);
You now get memoized callback functions cached and passed as props to the children components.
Finally, test the application to check that the issue has been resolved. On the first load of the application, all components are rendered. But when you click on the buttons only the components affiliated with that button render unlike the occurrence earlier on.
When you click on the update age button, only the Display
and Button
components related to that action re-renders.
When you also click on the update salary button, only the Display
and Button
components related to that action re-renders.
We have successfully optimized the application, improving its overall performance.
Conclusion
The useCallback
hook is a powerful tool that can be used to improve the performance of React components. By memoizing callback functions, useCallback
can prevent unnecessary re-renders, which can lead to a smoother user experience.
In this article, we explored the key concepts related to useCallback, including its purpose, usage, benefits, and best practices. We also compared useCallback with other related hooks like useMemo and highlighted their differences and appropriate use cases.
Here are some key takeaways from this article:
useCallback
is a hook that can be used to memoize callback functions.- Memoization is a technique that can be used to cache the results of a function call so that it does not need to be re-evaluated on every render.
- Memoization can be used to improve the performance of React components by preventing unnecessary re-renders.
useCallback
is most commonly used to memoize callback functions that are passed to child components.useCallback
can also be used to memoize callback functions that are used in other contexts, such as event handlers and timers.
When using useCallback
, it is important to be aware of the following best practices:
- Only use
useCallback
on callback functions that are truly performance-critical. - Specify the dependencies of
useCallback
carefully to ensure that the function is only memoized when necessary.
By following these best practices, you can use useCallback
to improve the performance of your React components without sacrificing code clarity or maintainability.
I hope this article has been helpful!
Resources
For further knowledge on the subject discussed, check these materials:
Originally published at https://semaphoreci.com on October 25, 2023.