11 Aug 2021
A Visual Guide to React Rendering - useCallback
This article is a 4th chapter of "A Visual Guide to React Rendering." Previous chapters: It always re-renders, Props, and useMemo.
You often see an event handler passed to a React component as an anonymous function. This causes the child component to re-render when the parent renders, even if you wrap the child in memo
. Let's figure out why.
Functions in JavaScript
JavaScript has first-class functions.
A programming language is said to have First-class functions when functions in that language are treated like any other variable. For example, in such a language, a function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.
When you pass an anonymous function like that, it's easy to overlook that onClick
is just a prop of a component, and the function you pass is just the value of this prop. Let's declare a variable with our function to make it easier to spot.
This way, it's a bit more obvious. The handler
stores a value for the onClick
prop. Whenever this value changes, the Child re-renders. We know from the second chapter that functions are non-primitive values and are compared by reference.
Every time Parent renders, the handler
is redeclared with a new reference pointing to a new value. This causes the Child to re-render. If we want to prevent that, we need to provide the same reference to the onClick
prop. To do that, we need to memoize the value of the handler
.
The useCallback hook
In the previous chapter, we explored useMemo
and how it caches the value instead of recalculating it on every render. The useCallback
hook is essentially the same. The only difference is that it returns a function.
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
So if we want to keep a reference for a handler
, all we need to do is wrap its value in useCallback
.
It stops the Child from re-rendering. But wait, why does the count
only update once? That's because of the dependency list. As with useMemo
, useCallback
will only recalculate its value when one of its dependencies changes. Since we have an empty list of dependencies, the handler
value is calculated only on the first render. And because of the closure, the memoized function will refer to the old value of count
, even when the count
changes. In our case, the count
will always be 0
. Therefore, count + 1
will always be 1
.
If you are not sure what the closure is, check out this article - MDN -Closures
So how do we make the count
update? We can put the count
in the list of dependencies. This way, useCallback
will recalculate the handler
value every time the count
changes and return the updated function with the latest lexical scope.
But now we're back where we started. The Child keeps re-rendering because the handler changes every time the count changes. To solve this, we can use functional updates.
Functional updates
If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value. Here鈥檚 an example of a counter component that uses both forms of setState:
The functional update allows us to remove count
from the dependency list without worrying about closures. The handler
will not be recalculated every time the count changes. And the prevCount
will always refer to the latest value.