How to use the React useCallback() hook
Published Aug 29 2022
The useCallback() hook is a function provided by React which can help boost performance. It allows us to memoize a function with the goal of preventing unnecessary component re-renders. Understanding when and why this is a problem is crucial to applying the useCallback() hook properly. So this post will cover why we need the useCallback() hook, how it works, and when exactly we should use it.
Why do we need the useCallback hook?
The useCallback() hook exists with the explicit purpose of solving issues around referential equality. To understand what that is, let’s look at some results the strict equality operator (===) gives us in JavaScript:
Code Playground
Hmmm.
When we declare any function in JavaScript, we create a new object in memory. And when we compare functions
(or other objects such as arrays or dates)
the expression will evaluate to true only if the compared values reference the same object instance in memory.
In contrast, primitives such as strings and numbers are compared by their value.
This is why function1 === function2 evaluates to false, despite the two functions being identical for all intents and purposes.
When to use the useCallback hook in React
How does all this relate to React? Well, it pertains to the relationship between a parent component and a child component. By default in React, if a parent component re-renders, all of its direct children will re-render:
This can sometimes result in sluggish performance due to expensive useless re-renders of children that have not actually changed, and can become particularly problematic with long lists. If we want to prevent this behaviour, we can wrap a component in React.memo(). This tells the child component not to re-render when the parent re-renders, unless the props it has received from the parent have changed.
Cool, so no more useless re-renders.
But wait... what if we want to pass a function from the parent to the child? Every time the parent re-renders, it will define the function again, and even if this function is otherwise identical to the previous function, it will reference a different place in memory. So when the child component runs its prop comparison, it sees that it has received a different function, and this triggers a re-render.
The generic case looks something like this:
Code Playground
In this example, even though ChildComponent is wrapped in React.memo(), it will rerender every time that ParentComponent rerenders, because the handlerFunction will get redefined.
In the real world, the issue most commonly arises when the parent component has some state that may change, but does not affect its children. Consider the following application:
Code Playground
It’s a somewhat contrived example, but it demonstrates the problem. Try typing a new character into the input field. Every time a user types a character into the form, the state of the parent component will change, causing it to re-render. That will result in the handlePressCharacter function being redefined, causing all of the child Character components to rerender unnecessarily, despite them being wrapped in React.memo().
It is in this situation that the useCallback() hook in React comes to the rescue. It takes two arguments: a function and an array of dependencies - useCallback(fn, [dependencies]). The useCallback() hook caches the function that it wraps and only creates a new one if the values in the dependency array change. If we leave the dependency array empty, it will cache the function on the first render and always reuse this cached function going forward.
Let's add it to our previous app:
Code Playground
Now when we type in the input field, the Character components do not rerender. Problem solved!
The dependency array should contain all values referenced inside of the wrapped function that can change. For example, if we wanted to use any of the state variables in the parent component, we would have to add them to the dependency array to ensure our function is changed when those values are updated:
const handlePressCharacter = useCallback((character) => {console.log(characters);console.log(formValue);}, [characters, formValue]);
When not to use the useCallback hook
Neat. We have learned how to use the useCallback hook.
"So should we just wrap all our handler functions with useCallback()?"
It's not that simple, I'm afraid. The cost of creating a new function and the child re-rendering might be relatively small, and using the useCallback hook comes with its own cost. It has to create an additional array and run logic on it's arguments under the hood. We have to use our judgement on a case-by-case basis. In cases where a simple function is passed to a single child, as in our first example, useCallback is usually unnecessary. It adds overhead and complexity and solves a performance problem that may not even exist yet.
An advisable approach is to wait until you actually see poor performance before optimising. If there is a long list that is needlessly re-rendering, causing slow performance on your app, the React.memo(). and useCallback() combo may be the solution. The React Developer Tools Chrome extension is a useful way for measuring any performance benefits (or lack thereof) from using the useCallback hook. More on that tool in another post.
Far out, right!?