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 double
function).
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 memoizedDouble(42)
, but 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 double
function.
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 useCallback
.
When memoizedCb
is called, it calls the original cb
callback, which eventually calls the double
function.
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 double
function.
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.
So, if 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!
What 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.