What do they mean by memoized callbacks and what does useCallback actually do?
When I first read that
useCallback returns a "memoized callback" I thought I knew what it meant.
I used both callback functions and memoized functions before so I was like "Ah, okay, it's just memoization.".
Well, it turns out that seeing the word "memoization" and immediately assuming that it means what it usually does was a mistake.
Much to my surprise,
useCallback does NOT return a memoized function.
According to Wikipedia, memoization is about limiting the number of expensive function calls by using some sort of a cache.
For example, this is how we can use
_.memoize from lodash to memoize a function.
First, we obtain a memoized version of a potentially expensive function (here it's just the
The first time we call
memoizedDouble(42), it calls the original
double function under the hood and returns its result, which is also stored in the cache.
The next time we call
memoizedDouble(42), the result is obtained from the cache. The original
double function is not called at all.
We can keep calling
double will never be called again. Unless the cache is cleared, the results will be fetched from there.
If there's no cached result for some input, the original function will be called once, and then the result will be added to the map.
To sum it up, two functions were created the original
double function and the
memoizedDouble function. Calling
memoizedDouble 4 times with 2 different inputs resulted in only 2 calls of the original
This was memoization. The
useCallback hook does something else. Let's see what's that.
Here we have a callback
cb that calls the
double function. We use
useCallback to get a memoized version of the
cb callback. The first time the component renders, a new
cb is created and the very same
cb function is returned by
memoizedCb is called, it calls the original
cb callback, which eventually calls the
The next time the component renders, another
cb is created. However, because the dependencies passed to
useCallback haven't changed, it's thrown away and
memoizedCb stays the same
cb it was during the first render.
The third time the component is rendered with the same dependencies, yet another
cb is created and then thrown away. Calling the
memoizedCb results in calling the same, old
cb, which then calls the original
double function again.
Unless the dependencies change, every render will create a new unused callback, and every call to the memoized callback, which is the old one, will actually call the original
When the dependencies finally change, the
memoizedCb also changes.
And similarly to how the previous
memoizedCb was calling the first
cb, this one also calls the first
cb it got since the dependencies changed.
To sum it up, 6 new callbacks were created during the 6 renders. Calling the memoized callback 4 times caused 4 calls of the original callback.
useCallback does neither limit the number of function calls nor functions being created, what is the value it provides?
What helped me to finally wrap my head around it it was to ignore the meaning of memoization. Let's pretend this word doesn't even occur in the documentation.
So far we've focused almost solely on the "memoized" adjective, but we haven't talked about "callbacks" yet. So, what's the purpose of a callback? It's a function that we pass down to some other function and we expect it to be called once something happens, like a query finished or an error occurs. It is a way to establish communication between various pieces of code. The module that calls the callback is not consuming the result, but the code passing the callback down expects to receive a call when an event occurs.
As we can see, side effects are the most important thing when it comes to callbacks. The results usually don't matter that much. What's crucial is that when one piece of code calls the callback, the other one receives the call. We don't want to lose any calls. That's why memoizing a callback would actually cut the communication link between modules in our application!
useCallback does is limit the number of identical callbacks floating around. As we can see in the picture visible above, across the first 4 renders, when the dependency was the number 42,
memoizedCb stayed exactly the same
cb function it was during the first render. It wasn't wrapped in anything. The results weren't returned from any cache. All what
useCallback ensured was that
memoizedCb equaled the first
cb since the last time the dependencies changed.
Does it improve the performance? Not on its own. It may actually decrease it a bit, if no other code relies on referential equality of such callbacks.