Łukasz Makuch

Łukasz Makuch

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. The Wikipedia definition of memoization

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).

A memoized 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.

A memoized function

The next time we call memoizedDouble(42), the result is obtained from the cache. The original double function is not called at all.

A memoized function

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.

A memoized function

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. A memoized function

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.

useCallback

When memoizedCb is called, it calls the original cb callback, which eventually calls the double function.

useCallback

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.

useCallback

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. useCallback

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. useCallback

When the dependencies finally change, the memoizedCb also changes. useCallback

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. useCallback

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.

From the author of this blog

  • howlong.app - a timesheet built for freelancers, not against them!