Intro
The useCallback hook is intended to improve performances by preventing recreating a function when it's not necessary.
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
It memoizes the function passed (1st argument) and only recreates it when one of the dependencies (2nd argument) has changed*.
🔗* React compares hook dependencies using
Object.is
(cf. areHookInputsEqual)
Why useCallback 🔮
That being said, we can deduce that the following question
Why should I use useCallback?
is equivalent to
Why should I avoid recreating a function?
For sure, the name of the hook gives a hint.
A JavaScript value is either
primitive
or an object. Functions are objects, which means they are referenced in memory.
Everytime we create a JavaScript function, we create a new reference.
And if a function f1
is created inside an other function f2
, a new reference is created for f1
everytime f2
is executed. Of course, the same applies to React function components : when
you declare a function f
inside a React function component, a new reference of f
is
created on each render.
And this is precisely what useCallback is here for : it prevents creating a new reference of
f
on each render so that anything that depends on f
will not be updated unnecessarily
(until anything f
depends on, changes).
🔗Being passed to other functions (such as components),
f
is used as acallback
, hence the nameuseCallback
When to use useCallback ✅
1. When an optimized child component receives a function as prop
The React documentation says
useCallback
is useful when passing callbacks to optimized child components that rely on reference equality
Such component is, at least, a memoized component, i.e. a component that is encapsulated
in React.memo()
.
If the child component is not memoized,
useCallback
brings no benefit since, by default, a React component re-renders everytime its parent does.
Let's say we want to create a component that allows to select multiple movies among a set of available movies.
To do so, we create a MoviesPicker
component that uses a generic List
component.
import React, { FC, memo, MouseEventHandler } from 'react';
type ListProps = {
items: string[],
onItemClick: MouseEventHandler<HTMLButtonElement>,
};
const List: FC<ListProps> = ({ items, onItemClick }: ListProps) => (
<ul>
{items.map((item) => (
<li key={item}>
<button data-item={item} onClick={onItemClick} type={'button'}>
{item}
</button>
</li>
))}
</ul>
);
export default memo(List); // export the memoized component
import React, { FC, useState } from 'react';
import List from './list';
type MoviesPickerProps = {
availableMovies: string[],
};
const MoviesPicker: FC<MoviesPickerProps> = ({ availableMovies }: MoviesPickerProps) => {
const [selectedMovies, setSelectedMovies] = useState<string[]>([]);
const handleMovieClick = (event) => {
const movie = event.currentTarget.dataset.item;
setSelectedMovies((prev) => [...prev, movie]);
};
return <List items={availableMovies} onItemClick={handleMovieClick} />;
};
export default MoviesPicker;
The List
component only receives the items
to render, and an onItemClick
callback.
Therefore, it has no reason to re-render when an item is clicked.
However, since the handleMovieClick
callback is recreated on each render,
the onItemClick
prop will always be a different function reference, forcing
the List
component to re-render everytime MoviesPicker
does.
If the number of items
to render is huge, it might induce performance problems,
especially because it recreates a new onClick
event handler for every list item on each render.
Encapsulating handleMovieClick
in useCallback
will prevent these unnecessary re-renders :
import React, { FC, useCallback, useState } from 'react';
import List from './list';
type MoviesPickerProps = {
availableMovies: string[],
};
const MoviesPicker: FC<MoviesPickerProps> = ({ availableMovies }: MoviesPickerProps) => {
const [selectedMovies, setSelectedMovies] = useState<string[]>([]);
const handleMovieClick = useCallback((event) => {
const movie = event.currentTarget.dataset.item;
setSelectedMovies((prev) => [...prev, movie]);
}, []);
return <List items={availableMovies} onItemClick={handleMovieClick} />;
};
export default MoviesPicker;
As you can see, the dependencies of useCallback
(2nd argument) is an empty array :
it means handleMovieClick
will never be recreated through renders.
useCallback
does nothing when called with only one argument : don't forget to pass the array of dependencies, even if there are none!
Let's say that now, we cannot select more than a certain number of movies. To do so,
we pass a max
number to MoviesPicker
as a prop.
The following exemple is a bit far-fetched, it would probably be better not to set a click handler if
max
is already reached : please do not care about the implementation but rather focus on the principle.
import React, { FC, useCallback, useState } from 'react';
import List from './list';
type MoviesPickerProps = {
availableMovies: string[],
max: number,
};
const MoviesPicker: FC<MoviesPickerProps> = ({ availableMovies, max }: MoviesPickerProps) => {
const [selectedMovies, setSelectedMovies] = useState<string[]>([]);
const handleMovieClick = useCallback((event) => {
const movie = event.currentTarget.dataset.item;
setSelectedMovies((prev) => (prev.length < max ? [...prev, movie] : prev));
}, [max]);
return <List items={availableMovies} onItemClick={handleMovieClick} />;
};
export default MoviesPicker;
handleMovieClick
now depends on max
.
Therefore, max
has to be passed in the array of dependencies, otherwise useCallback
will not recreate the function if ever max
changes, and will use its previous (outdated) value.
2. Whenever you want to reuse the same function instance
More generally, useCallback
can be used for functions facing reference equality at some point.
import React, { FC, useEffect } from 'react';
const MyComponent: FC = () => {
const doSomething = () => {
// ...
};
useEffect(() => {
doSomething();
}, [doSomething]);
return (
<p>I run a side effect 🧙♂️</p>
);
};
export default MyComponent;
For example, this useEffect
depends on doSomething
, so we include it to the array of dependencies.
It means every time doSomething
changes, the side effect will run again.
But as doSomething
is a new function reference on each render, the side effect will run on each render.
In this case, eslint-plugin-react-hooks
comes to the rescue with the following error message :
🔗The
doSomething
function makes the dependencies ofuseEffect
hook change on every render. Move it inside theuseEffect
callback. Alternatively, wrap the definition ofdoSomething
in its ownuseCallback()
hook.
When not to use useCallback ❌
1. When the child component is not memoized
Remember : the default behaviour of a child component is to re-render whenever its parent does, no matter if its props have changed or not. Therefore, it has to be memoized in order to skip potential re-renders.
Also, some non-pure components, for instance, cannot be memoized : they may need to re-render even if their props have not changed.
This is the case of the
DiceRoller
component I describe in this blog post : What is a React pure component?
In this case, useCallback
is of no use because the child component will re-render
no matter if the callback it receives as prop is the same function reference or not.
2. When the complexity is not worth it
useCallback
comes with its own complexity, and might not always bring benefit.
Profiling surely is the best option to really know whether it actually enhances performances or not.
We can use the React Profiler API
to analyze the duration (and estimate the "cost") of a re-render without useCallback
.
import React, { FC, Profiler, ProfilerOnRenderCallback, useState } from 'react';
import List from './list';
type MoviesPickerProps = {
availableMovies: string[],
};
const profile: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
console.log(actualDuration);
};
const MoviesPicker: FC<MoviesPickerProps> = ({ availableMovies }: MoviesPickerProps) => {
const [selectedMovies, setSelectedMovies] = useState<string[]>([]);
// no useCallback, as we want to measure the "cost" of a re-render without it.
const handleMovieClick = (event) => {
const movie = event.currentTarget.dataset.item;
setSelectedMovies((prev) => [...prev, movie]);
};
return (
<Profiler id={'list'} onRender={profile}>
<List items={availableMovies} onItemClick={handleMovieClick} />
</Profiler>
);
};
export default MoviesPicker;
For this example, the render duration of a list of :
100
movies :3
ms1000
movies :15
ms10000
movies :150
ms100000
movies :1000
ms1000000
movies : let's not kill my CPU.
In other words, the render duration of a list of 1000
elements (which is already quite huge),
remains fast. Usually, performance problems of such big list are tackled
with virtualization, rather than using "manual" optimization like useCallback
.
Still, this example uses very simple components, there is no golden rule :
if you want to know if useCallback
would be useful in a certain case, you should profile !
But it is likely that, in most cases, you don't need to bother optimizing re-renders with it.
Conclusion
useCallback
will help preserving reference equality when it's necessary,
such as preventing undesired re-run of a side effect that depends on a callback, for example.
It also makes the code a little more complex, and might not always be worth it in simple cases for performances improvement.
In such cases, the best way to really know if it brings any performance benefit is to profile, using, for example, the React Profiler API.