When to use useCallback

July 2021
🔗

Intro

The useCallback hook is intended to improve performances by preventing recreating a function when it's not necessary.

Example from the React documentationJS
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 a callback, hence the name useCallback

🔗

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.

list.tsxTSX
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
movies-picker.tsxTSX
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 :

movies-picker.tsxTSX
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.

movies-picker.tsxTSX
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.

TSX
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 of useEffect hook change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of doSomething in its own useCallback() 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.

movies-picker.tsxTSX
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 : 3ms
  • 1000 movies : 15ms
  • 10000 movies : 150ms
  • 100000 movies : 1000ms
  • 1000000 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.

Made with  ❤️  from Saumur