useCallback: When should we use?

RMAG news

How to Use useCallback
useCallback is a Hook provided by React to memoize functions. This Hook is useful when you want to reuse a specific function without recreating it. useCallback takes a function as the first argument and a dependency array as the second argument. It stores the function and reuses it until one of the values in the dependency array changes.
Here is an example where { setCount(c => c + 1); } is the first argument and [count] is the second argument.

import React, { useCallback, useState } from ‘react’;

function MyComponent() {
const [count, setCount] = useState(0);

// The function is recreated only when ‘count’ changes.
const increment = useCallback(() => {
setCount(c => c + 1);
}, [count]);

return (
<div>
Count: {count}
<button onClick={increment}>Increase</button>
</div>
);
}

It’s important to include all the state and props used within the function in the dependency array. If the dependency array is set incorrectly, the function might reference outdated values instead of the latest ones.
Example Comparing useCallback and useState
Here is an example showing the performance differences between using useCallback and useState, particularly focusing on rendering performance.
Suppose we have a simple component that adds items to a list using useState for state management and memoizes the item-adding function with useCallback.

import React, { useState, useCallback } from ‘react’;

function ListComponent() {
const [items, setItems] = useState([]);

// Memoize the addItem function using useCallback.
const addItem = useCallback(() => {
setItems(prevItems => […prevItems, ‘New Item’]);
}, []);

return (
<div>
<button onClick={addItem}>Add Item</button>
{items.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
}

In the above code, the addItem function is memoized with useCallback, which optimizes performance by reusing the same function rather than creating a new one each time the items array changes.
In contrast, if we declare the function directly without useCallback, a new function is created on every render.

import React, { useState } from ‘react’;

function ListComponent() {
const [items, setItems] = useState([]);

// Declare the addItem function directly without useCallback.
const addItem = () => {
setItems(prevItems => […prevItems, ‘New Item’]);
};

return (
<div>
<button onClick={addItem}>Add Item</button>
{items.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
}

In this case, the addItem function is recreated every time the component re-renders. If this function is passed as a prop to a child component that uses React.memo or PureComponent, it can cause unnecessary re-renders of the child component.
Example of Unnecessary Re-renders Without useCallback
When a parent component recreates a function and passes it to a child component as a prop, the child component might re-render because it receives a new prop reference. This can happen if useCallback is not used.
For example, in the code below, ParentComponent recreates the handleClick function on every render. Although ChildComponent is wrapped with React.memo, it re-renders whenever ParentComponent re-renders because the handleClick function reference changes.

import React, { useState } from ‘react’;

const ChildComponent = React.memo(({ onClick }) => {
console.log(‘ChildComponent is rendering…’);
return <button onClick={onClick}>Click Me</button>;
});

const ParentComponent = () => {
const [count, setCount] = useState(0);

// A new function is created on every render.
const handleClick = () => {
setCount(c => c + 1);
};

return (
<div>
<ChildComponent onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
};

In the above code, handleClick is recreated each time the count state changes, causing ChildComponent to re-render even though it’s wrapped with React.memo.
To solve this, you can use useCallback to memoize the handleClick function:

import React, { useState, useCallback } from ‘react’;

const ChildComponent = React.memo(({ onClick }) => {
console.log(‘ChildComponent is rendering…’);
return <button onClick={onClick}>Click Me</button>;
});

const ParentComponent = () => {
const [count, setCount] = useState(0);

// Memoize the handleClick function using useCallback.
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, [setCount]); // Include setCount in the dependency array.

return (
<div>
<ChildComponent onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
};

Now, handleClick is not recreated unless setCount changes, preventing unnecessary re-renders of ChildComponent. Using useCallback appropriately can help optimize performance.