I write a lot of React components which require code review. Many times during that process someone has dropped the classic line:
I think we should wrap this function in
useCallback
for performance reasons.
But what is useCallback
and when does it make sense to use it?
useCallback refresher
To recap, useCallback
is a React Hook which returns a memoized version of the callback function it is passed.
This means that the function object returned from useCallback
will be the same between re-renders.
Why does this matter?
It’s worth recalling that in JavaScript, functions display referential equality. This means they are only considered equal if they point to the same object.
Therefore if you were two declare two functions with identical implementations they would not be equal to each other.
For example:
const firstFunction = function() {
return 1 + 2; // same as secondFunction
}
const secondFunction = function() {
return 1 + 2; // same as firstFunction
}
// Same implementation, different objects
firstFunction === secondFunction; // false
// Same objects!
firstFunction === firstFunction; // true
When is this useful in React?
Typically useCallback
is helpful when passing callback props to highly optimised child components.
For example, if a child component that accepts a callback relies on a referential equality check (eg: React.memo()
or shouldComponentUpdate
) to prevent unnecessary re-renders when its props change, then it is important that any callback props do not change between renders.
To do this, the parent component can wrap the callback prop in useCallback
and be guaranteed that it is passing the same function object down into the optimised child component.
function ParentComponent() {
const onHandleClick = useCallback(() => {
// this will return the same function
// instance between re-renders
});
return (
<MemoizedSubComponent
handleClick={onHandleClick}
/>
);
}
For more on this see my article Avoiding React component re-renders with React.memo.
When not to use useCallback
You should avoid seeing useCallback
as a blanket performance optimisation.
In most cases, it’s simply better to accept that for functions declared inline with React components, each new re-render creates a new function object. This is typically absolutely fine and will not have a detrimental impact on performance.
You should always profile though – just in case!
In fact, as Kent C Dodds explains, in many cases the overhead of applying useCallback
unnecessarily actually worsens the performance!
Remember: useCallback doesn’t memoize the function result
It’s worth noting that applying useCallback
doesn’t memoize the result of a function’s invocation. Rather it memoizes the function object itself.
Consider the following:
function ParentComponent() {
const onHandleClick = useCallback(() => {
const special = calculatePi();
});
return (
<SubComponent
handleClick={onHandleClick}
/>
);
}
In this example, each time the <SubComponent>
triggers the onHandleClick
callback via its handleClick
prop the (presumably expensive!) calculatePi()
will be triggered. The result of the arrow function isn’t memoized, only the reference.
If we wanted to avoid re-calculating Pi on each handleClick
we’d be better off memorizing calculatePi
directly via useMemo()
:
const memoizedPi = useMemo( calculatePii() );
Further reading
For more on useCallback
I recommend taking a look at the following articles and guides:
- Your Guide to React.useCallback() by Dmitri Pavlutin.
- When to useMemo and useCallback by Kent C. Dodds
- How to avoid passing callbacks down? from the Official React docs.
This is fantastic, thank you! I’ve been struggling to understand what useCallback is _actually_ used for, and now I have a good grasp 🙂 Much appreciated!
great explanation, thanks a lot