Introduction
The useCallback hook is used to optimize and memoize (cache) a callback function in React. By memoizing the callback function we can prevent unnecessary re-rendering of the child components.
In this article, we will explore how to use the useCalllback to improve the performance of our application.
Using useCallback hook
Syntex:
const cachedFn = useCallback(fn, dependencies);
// import useCallback from react
import { useCallback } from 'react';
function MyComponent(){
const cachedFun = useCallback(
() => { },
[a, b]
);
// ...
}
The useCallback takes two arguments:
A function that we want to catch: The useCallback will return the same function during re-render if dependencies haven't changed. If any of the dependencies changed, then the useCallabck will return a new function.
dependencies array: This array contains all the variables that are referenced inside the function like the state, props and all variables that are present in the component body.
Note that the useCallback doesn't call the function that we pass as an argument, it cashes the function.
The problem that useCallback solves
In JavaScript, variables inside of a function get created when we call the functions, and they get destroyed when the function completes execution.
In each function call, variables get a different memory address. They may have the same content, but under the hood, they are different variables.
In React, every time a component is rendered, variables inside the component body are created, and when the component re-renders, all the variables are destroyed and created again.
Creating and destroying variables don't affect the component's performance. But problems arise when we pass those variables to the child components as props. As we know that a component re-render if its props change.
To solve this problem we use the useCallback, which caches the variables, and prevent the creation of variables unnecessarily at every render.
Let's understand it with an example.
import React, { useState } from "react";
export const MyCounter = () => {
const [count, setCount] = useState(0);
console.log("MyCounter render");
const handleClick = () => {
setCount((preCount) => preCount + 1);
};
return (
<>
<p> Count value is :{count}</p>
<ChildCounter handleClick={handleClick} />
</>
);
};
const ChildCounter = ({ handleClick }) => {
console.log("ChildCounter render");
return (
<>
<button onClick={handleClick}> Increment</button>
</>
);
};
In this example, We are passing a handler function from the ParentCounter
component to the ChildCounter
component. In the ChildCounter
component, we call this function when the Increment button is clicked.
Clicking the Increment
button updates the count state variable in the ParentCounter
component, and updating the count state triggers a re-render. Whenever ParentCounter
re-renders, its child component, ChildCounter
, also re-renders.
In React when the parent component re-renders, all children also re-render. But sometimes we don't want to re-render child components. In the above example, it is unnecessary to re-render the ChildCounter
component when ParentCounter
re-renders.
We can prevent this behaviour using useCallback and React.memo()
.
What is React.memo()?
React.memo()
is a higher-order component(HOC) provided by React. It is used to optimize the performance of a functional component by memoizing its rendered output.
When a component wrapped in React.memo()
, receives a new prop, React will compare the previous props with the new props. If they are the same, React will use memoized version of the component, and avoid re-rendering it. This can be beneficial when the component's rendering is computationally expensive or when re-rendering is unnecessary because the component's output would be the same.
Here's an example of how to use it:
import React from 'react';
const MyComponent = React.memo((props) => {
// Component logic and rendering
});
export default MyComponent;
In this example, MyComponent
is wrapped with Ract.memo()
. React will automatically memoize the component based on the equality of its props. If the props remain the same between renders, the component will not be re-rendered and thus improving the performance of the application.
Using useCallback to solve the problem
Let's write the counter example with useCallback()
and React.memo()
, to prevent unnecessary rendering of the ChildCounter
component.
The ChildCounter
takes the handleClick
function as props. This handleClick
function is recreated every time the ParentCounter
component re-render, so to prevent it, we can use the useCallback
. Now every time the ParentCounter
component re-render clickHandler
remains the same.
Now, we need to wrap the ChildCounter
component with React.memo()
. As we know that React.memo()
memoized the wrapped component, and only re-render the component if the props change.
import React, { useState, useCallback } from "react";
export const MyCounter = () => {
const [count, setCount] = useState(0);
console.log("MyCounter render");
const handleClick = useCallback(() => {
setCount((preCount) => preCount + 1);
}, []);
return (
<>
<p> Count value is :{count}</p>
<ChildCounter handleClick={handleClick} />
</>
);
};
// Wrap ChildCounter component with React.memo()
const ChildCounter = React.memo(({ handleClick }) => {
console.log("ChildCounter render");
return (
<>
<button onClick={handleClick}> increment</button>
</>
);
});
Best practices
When using the useCallback
hook, there are some best practices that we should keep in mind:
Identify expensive computations: Use the useCallback to memoize callback functions that involve expensive computations or operations. This prevents unnecessary re-creation of callback functions on each render.
Pass dependencies in the dependency array: Make sure to include all the dependencies that the callback function relies on in the dependency array.
This ensures that the callback function is re-created only when the dependencies change.