All about React.useCallback()


The Basic Idea
Memoization: What is Memoization?
The most common answer you find on the internet is: Memorization makes applications faster by reducing the need for recompilation. It does this by storing computation results in a cache and retrieving that same information from the cache the next time it's needed instead of computing it again. Simple implementation to get an idea of this
const store = {}
function incrementOne(input) {
if (store[input]=== undefined) {
store[input] = input + 1
}
return store[input]
}
The basic idea is to avoid recalculating a value for which you already have a cached result, which means returning the cached output when called with
const a = incrementOne(2) // output: 3
const b = incrementOne(1) // output: 2
const c = incrementOne(2) // output: 3 --> this time it is from the cache
Let's talk about one more thing which really helps in understanding useCallback clearly i.e referential equality between functions.
function incrementOne(input) {
return input + 1
}
const a = incrementOne(2) // output: 3
const b = incrementOne(2) // output: 3
// now let's check what we get if we compare these two functions
a===b // output: false
In JavaScript, functions are treated as regular objects, so they are considered first-class citizens, Even though the functions a
and b
share the same code, comparing them a === b
evaluates to false
as their references are different. That's just how JavaScript objects works, an object (including a function object) equals only itself.
a===a // output: true
a===b // output: false
Now looking at another aspect of memoization is value referential equality. For Example, If We use memoized incrementOne
then it will point to the same reference from the cache.
const store = {}
function incrementOne(input) {
if (store[input]=== undefined) {
store[input] = input + 1
}
return store[input]
}
const a = incrementOne(2) // output: 3
const b = incrementOne(1) // output: 2
const c = incrementOne(2) // output: 3 --> this time it is from the cache
a===c // output: true
Memoization is the thing you can implement as a generic abstraction. Thankfully, in React we do not have to implement a memoization abstraction. They made two for us! useMemo and useCallback, for now, let's check useCallback.
** Remember: useCallback doesn’t memoize the function result, rather it memoizes the function object itself.**
1. The purpose of useCallback()
Different function objects sharing the same code are often created inside React components
function MyComponent() {
const updateLocalStorage = () => window.localStorage.setItem('timer', timer)
React.useEffect(() => {
updateLocalStorage()
}, []) // <-- what goes in that dependency list?
}
We could just put the timer
in the dependency list and that would intentionally or unintentionally work, but what do you think would happen if one day someone were to change updateLocalStorage
? like changing the 'timer'
to key
to accept it as an argument.
const updateLocalStorage = () => window.localStorage.setItem(key, timer)
Would we remember to update the current dependency list to include the key
? mostly yes. However, it can be difficult to keep track of dependencies, perhaps not in this instance but especially if these kinds of functions are being used as arguments or props
So it would be much easier if we could just put the function itself in the dependency list like:
function MyComponent() {
const updateLocalStorage = () => window.localStorage.setItem('timer', timer)
React.useEffect(() => {
updateLocalStorage()
}, [updateLocalStorage])
}
With this, We don't have to worry about keeping track of changes in the updateLocalStorage function or dependency names, etc.
All good now? we can leave it like this but take a guess at what's going to happen if we do leave it like this, it will trigger the useEffect
to run every render. This is because updateLocalStorage
is defined inside the MyComponent function body and it is a different function object on every rendering of MyComponent
. This means it's re-initialized every render. Which means it's brand new every render. Which means it changes every render. This results in calling our useEffect
on every render as we have updateLocalStorage
as a dependency.
**This is the problem useCallback() solves. And here's how you solve it **
const updateLocalStorage = React.useCallback(
() => window.localStorage.setItem('timer', timer),
[timer], /** <-- That's a dependency list! given the same dependency values,
the hook returns the same function instance between renderings. */
)
React.useEffect(() => {
updateLocalStorage()
}, [updateLocalStorage])
Wait! what? we pass a function to React and it gives that same function back to us!
// this is only for representation
function useCallback(callback) {
return callback
}
like this? no, it doesn't just return a callback.
What it does is, on subsequent renders, if the elements in the dependency list are unchanged (here timer
), while we still create a new function every render (to pass to useCallback), but instead of giving the same function back that we give to it, React will give us the same function it gave us last time( aka memoization).
// this is only for representation
let lastCallback
function useCallback(callback, deps) {
if (depsChanged(deps)) {
lastCallback = callback
return callback
} else {
return lastCallback
}
}
In the above example, if the dependency list doesn't change then the theupdateLocalStorage
variable has always the same callback function object between renderings of MyComponent
So, now does every callback function (like updateLocalStorage
) be wrapped with useCallback()
? to optimize the re-rendering or to prevent useless re-rendering, be it the child components that use any callback or the component itself?
This is far from the truth! let's check when to and when not use.
2. When to use useCallback()
Different function objects sharing the same code are often created inside React components
function MyComponent() {
// handleClick is re-created on each render
const handleClick = () => {
console.log('Clicked!');
};
// ...
}
We now know that handleClick
is a different function object on every rendering of MyComponent.
In some cases, you may need to maintain a single function instance between renderings, for instance when the task is computationally intensive and also
- When the function is one of the dependencies array of the useEffect eg:
useEffect(..., [callback])
(updateLocalStorage)
The callback of an useEffect gets called on the first render and every time one of the variables inside the dependency array changes. And since normally a new version of that function is created on every render, the callback might get called infinitely. So useCallback is used to memoize it.
- When the function as prop is being passed to one of the children's components and it is wrapped with
React.memo
. Especially when it is being called on their useEffect hook.
A memoized component with memo re-renders only if its state or props changes, not because its parent re-renders. And since normally a new version of that passed function as props is created, when the parent re-renders, the child component gets a new reference, hence it re-renders. So useCallback is used to memoize it.
When the function has some internal state.
When you use React
Context
that holds a state and returns only the state setters functions, you need the consumer of that context to not rerender every time the state update
3. When not to use useCallback()
We don't want to wrap every function with useCallback()
.
Remember that useCallback()
hook is called every time the component renders, and it will still create a new function object every render (to pass to useCallback
), butuseCallback()
just skips it and gives us the old one if the dependency list is not changed.
So, by using useCallback() you also increased code complexity. And also you have to keep track of all the deps
and be in sync.
If your components are lightweight then their re-rendering doesn't create performance issues, and few inline functions per component are acceptable because creating those is really cheap, If the tasks are not computationally heavy, it may be better to solve them in another way, or leave it alone, with few inline functions the re-creation of functions on each rendering is not a problem.
Performance optimizations are not free, a more expensive optimization is better than a less expensive one. Therefore, optimize responsibly.
For further reading, I suggest you read the article by Kent C. Dodds on When to useMemo and useCallback
Subscribe to my newsletter
Read articles from Shrihari Bhat directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by