Skip to main content

How and when to use React useCallback()

15 mins 📖 📖 📖 📖 📖 📖 📖 📖 

react

In short, React's useCallback hook is used to wrap functions. It tells React to not re-create a wrapped function when a component re-renders, unless any of the useCallback's dependencies change. But when is it necessary to use useCallback?

Most blog posts I have read on useCallback contain example code where a function wrapped in useCallback could just be moved outside of a component function body and into its own scope. Or placed into a useEffect instead. React is a smart library that is optimised to not need a hook like useCallback in most situations.

The example code in this post aims to be more "real-world". Because of this, it's necessarily quite complex. As well as using common React hooks such as useState, useEffect, it also uses a number of JavaScript methods such as the fetch API, promises, filtering, splicing, destructuring, and currying.

Even if you aren't an expert in all of the methods used in the example code, I hope you can still learn something!

I want to explain an important fundamental JavaScript concept that will make understanding useCallback easier—object references:

Functions are objects in JavaScript. Even if two functions are identical, they won't equal each other:

var dog1 = func(){console.log('14/10')}; // has a unique object reference
var dog2 = func(){console.log('14/10')}; // has a unique object reference

dog1 == dog2; // false
dog1 === dog2; // false

In comparison, if an object assigned to a variable is directly assigned to another variable, the references will match:

var dog1 = func(){console.log('14/10')}; // has a unique object reference
var dog2 = dog1; // assign the unique object reference of dog1 to a variable named dog2

// dog1 and dog2 point to same object reference
dog1 == dog2; // true
dog1 === dog2; // true

In the next section, we'll see why object references are fundamental to writing and understanding React apps.

This section will go through and explain each step of a dog park example app . If you want to take a look at the final code, here is the Dog Park GitHub repository. If you want to see a live version of the app, here is the Dog Park app.

The initial features that I built into the dog park app were pretty cool. They let you set a name for your park and choose the number of dogs in it!

Inside the function body of the DogPark component, there is a function called fetchDog. This function fetches an array of dogs from The Dog API by Postman. DogPark re-renders whenever a user interacts with any of its elements, including its child component, Dogs. Whenever DogPark re-renders, fetchDog will be re-created and receive a new object reference.

import React, { useState, useCallback } from 'react';
import Dogs from './Dogs';
import shuffle from './shuffle';

const DogPark = () => {
const [text, setText] = useState('');

const handleText = (event) => {
setText(event.target.value);
};

// Gets a new object reference when it is re-created.
// It is re-created whenever DogPark re-renders.
const fetchDog = (number) => {
const result = fetch(`https://api.thedogapi.com/v1/breeds/`)
.then((response) => response.json())
.then((json) =>
shuffle(json).splice(0, number)
);

return result;
};

return (
<>
<h1>Welcome to {text || 'The Dog Park'}!</h1>
<p>
<label>
Name your dog park:
{' '}
<input type="text" value={text} onChange={handleText} />
</label>
</p>
<p>Add the perfect Dogs to your park! Maximum of 10.</p>
<Dogs onFetchDog={fetchDog} />
</>
);
};

export default DogPark;

Let's take a look at the Dogs component:

import React, { useEffect, useState } from 'react';

const Dogs = ({ onFetchDog }) => {
const [number, setNumber] = useState(1);
const [dogList, setDogList] = useState([]);

// Runs the "fetchDog" function when either the number
// variable or the onFetchDog variable changes.
useEffect(
() => {
if (number && typeof onFetchDog === 'function') {
async function fetchDog() {
const response = await onFetchDog(number);
setDogList(response);
}
fetchDog();
}
},
[onFetchDog, number] // dependencies of the useEffect
);

return (
<>
<label>
Number of dogs:
{' '}
<input
max="10"
min="1"
value={number}
type="number"
onChange={(event) => setNumber(event.target.value)}
/>

</label>
{dogList && (
<ul>
{dogList.map((dog) => (
<li key={dog.id}>{dog.name}</li>
))}
</ul>
)}
</>
);
};

export default Dogs;

The useEffect in the Dogs component has in its dependency array the fetchDog function (which has been passed down as onFetchDog), and the numbers variable.

An input with a type of number lives inside the Dogs component. Whenever the number of dogs is changed, Dogs will re-render and fetchDog will be run. This is good! It's what we want. Note: when state that lives inside a child component changes and the child is re-rendered, this will not trigger a re-render of the parent component.

If state that lives inside the parent component changes and the parent is re-rendered, the child component will also re-render. In the majority of cases, this is normal, expected behaviour. In the small number of cases this is causing an obvious rendering issue, you can try wrapping the child component in React.memo. However, if a value in the parent component that the child component depends on gets a new object reference, React.memo won't work. In our app, Dogs depends on the fetchDog function coming from DogPark.

Whenever a character is typed into the "Name your dog park" input in DogPark, DogPark will re-render and fetchDog will be re-created and get a new object reference. Dogs will also re-render and because the fetchDog dependency in its useEffect has changed, the useEffect will trigger, and the fetchDog function will run. This means that the list of dogs inside Dogs will refresh every time a single character is typed into the "Name your dog park" input. That is not good! It's not what we want. But what can we do?

We could wrap the fetchDog function inside DogPark into a useCallback to ensure it is not re-created each time DogPark re-renders. However, as the fetchDog function has no dependencies, it can safely be moved out of the function body of DogPark. This is a simpler way to ensure that fetchDog is not re-created every time DogPark re-renders:

// This function now lives outside of the DogPark function
// body and so is not re-created whenever DogPark re-renders
const fetchDog = (number) => {
...
};

const DogPark = () => {
... // DogPark function body

Ok, so, useCallback wasn't needed. But now, a third feature is going to be added to the app that is going to require useCallback. This feature will be the ability to choose dogs that have names beginning with either A-M or N-Z.

A new state variable and two radio buttons are added. And the fetch function is moved back into DogPark and altered a little:

const DogPark = () => {
const [text, setText] = useState('');
// New state variable
const [charRange, setCharRange] = useState('A-M');

const handleText = (event) => {
setText(event.target.value);
};

const fetchDog = (number) => {
const result = fetch(`https://api.thedogapi.com/v1/breeds/`)
.then((response) => response.json())
.then((json) =>
shuffle(
// Filters dogs depending on the value of
// the new state variable "charRange"
json.filter((dog) => {
return charRange === 'A-M'
? dog.name[0] < 'N'
: dog.name[0] > 'M';
})
).splice(0, number)
);
return result;
};

return (
<>
<h1>Welcome to {text || 'The Dog Park'}!</h1>
<p>
<label>
Name your dog park:
{' '}
<input type="text" value={text} onChange={handleText} />
</label>
</p>
<p>Add the perfect Dogs to your park! Maximum of 10.</p>

{/* Two new radio buttons */}
<p>
<label>
A-M
<input
type="radio"
checked={charRange === 'A-M'}
onChange={() => setDogHalf('A-M')}
/>

</label>
<label>
N-Z
<input
type="radio"
checked={charRange === 'N-Z'}
onChange={() => setDogHalf('N-Z')}
/>

</label>
</p>
<Dogs onFetchDog={fetchDog} />
</>
);
};

The fetchDog function now relies on the charRange state that lives within DogPark. This means fetchDog has to live in the function body of DogPark. I thought I could solve this issue by passing charRange to the fetchDog function that's passed down to Dogs:

// Here, fetchDog is outside of DogPark and gets the
// charRange state as a curried value but the returned
// function is still re-created each time DogPark re-renders
const fetchDog = (charRange) => (number) => {
...
};

const DogPark = () => {
...
<Dogs onFetchDog={fetchDog(charRange)} />
};

Even though I successfully moved fetchDog out of DogPark, fetchDog is still re-created every time DogPark re-renders.

So, fetchDog needs to stay within DogPark, and useCallback can help to avoid fetchDog being re-created every time DogPark re-renders. This means that when a character is typed into the "Name your dog park" input, even though DogPark re-renders, fetchDog keeps its object reference, and so the useEffect in Dogs is not triggered. And the dog list in Dogs is not needlessly refreshed!

// Now the fetchDog function is wrapped in the 
// useCallback hook, with "charRange" in the hook's
// dependency array.
const fetchDog = useCallback(
(number) => {
const result = fetch(`https://api.thedogapi.com/v1/breeds/`)
.then((response) => response.json())
.then((json) =>
shuffle(
json.filter((dog) => {
return charRange === 'A-M'
? dog.name[0] < 'N'
: dog.name[0] > 'M';
})
).splice(0, number)
);

return result;
},
[charRange]
);

When to actually use useCallback

In most use cases, your application won't be affected if a function is re-created and gets a new object reference upon each render. Even so, it can be tempting to proactively wrap a function in a useCallback to improve app performance. However, this premature optimisation can actually do harm rather than doing good. A blog post by Kent Dodds explains when and when not to use useCallback.

A good way to approach using useCallback is reactively rather than proactively. This means that, depending on your components, use it when you obviously need to, and not as a premature performance optimization. In short, don't wrap every function living inside a function body in a useCallback.

It's highly recommended that you have React linting in your development environment, so that your linter can suggest appropriate times to use useCallback.

If your linter is not suggesting useCallback, but you see that your UI is re-rendering in unexpected ways (as in the example in this post), or you have an infinite loop, check to see whether useCallback helps.

Buy me a coffee ❤️

18 Responses

 
Ana RodriguesOfaliceEva - includeJS.dev 🌈Dion

1 Repost

Dion

4 Replies

  1. rozenmd rozenmd
    Awesome article, big fan of using real examples instead of yet another to-do list or counter app. Some feedback: There's a few points in the code examples where you're trying to draw attention to certain lines - look into using something like gatsbyjs's //highlight-next-line Ditch the GitHub repo in the future (for your own sake - it's a ton of effort, and a CodeSandbox would probably get more interaction) Use more headings - makes it easier to skim/figure out the parts I want to actually read slowly and think through I'd probably lead with the example where you do require useCallback, and then show examples that don't need it (avoid leading with exceptions to your topic in general)
  2. wowzers5 wowzers5
    Fairly thorough, not a bad read! Couple of quick thoughts - You mention unwanted re-renders, but don't actually note how to observe that. A brief note on React DevTools and profiling would go well with your "using (useCallback) reactively" note. There's a lot of additional context not explicitly related to useContext here. While that's helpful for less experienced engineers, I generally will skip out of an article if I can't clearly and quickly see that it's offering more than what the official documentation will. I don't have a solid suggestion here, but something to keep in mind to keep readers engaged throughout. More of a code nit (sorry!) but your final version of DogPark could be optimized a bit more, since the `/breeds` endpoint isn't going to change data, you could store that in state and save the browser from additional network calls on every toggle or dog limit change. It separates the "Let me get this data" from the "Filter the data I want to see" concerns to reduce network traffic. With a post focusing on optimization, I'd do as much as possible (without going overboard obviously). Overall, it's a solid post and has obviously had some thought put into it. +1 for having a public working example, visualizing and interacting with something can really help understanding new topics.
  3. freneticpony21 freneticpony21
    Cool article! My understanding of useCallback was solidified for sure. Your code examples (<code> blocks) are showing up inline on my browser which makes things difficult to read.
  4. gtmkmr gtmkmr
    Well written. 👍

1 Mention

  1. Tobias Ljungström Tobias Ljungström
    God article on when to use useCallback in React, by @ambrwlsn90 amberwilson.co.uk/blog/how-and-w… #react #webdev