A Deep Dive Comparison Between useMemo And useCallback
March 07, 2022 - 6 minutes
One of the greatest things that we received in the world of React development is hooks. The introduction of React hooks back in 2019 enabled a totally new way of handling logic. Hooks significantly improved how we could implement, structure, isolate and share logic across different components.
Even to this day, 3 years later at the moment of writing this article, I’m discovering and learning new things about certain hooks or their internals. One of the things that recently intrigued my curiosity were two React hooks that I’ve been using quite frequently: useMemo
and useCallback
.
Their Purpose
The purpose behind both hooks is to improve React performance through memoization. In short, memoization is a caching technique that stores computed values based on the input. If the function is called with the same input, then it will skip the computation and return the cached value. If the input changes, it will recalculate the value and cache it.
Both useMemo
and useCallback
allow React developers to easily memoize certain entities, which can be used to potentially prevent component re-renders. The useCallback
hook is specifically meant for memoizing callbacks, as the name suggests. The useMemo
hook can be used for all the other, static entities like primitives and objects.
The Similarities
In practice, the useMemo
and useCallback
hooks don’t only share the same purpose but are also extremely similar in usage. Both of the hooks:
- have to be called at the top-level of a component, just like all hooks for that matter.
- accept a function as their first argument and an array of dependencies as their second argument.
- return a memoized value that is referentially stable across renders based on the function that is provided.
- recalculate and memoize the value based on the function when any changes are detected in the dependencies array.
Are There Differences?
So then the interesting question becomes: are there differences between useMemo
and useCallback
? If so, what are they exactly?
As established already earlier in this article, a small difference is that useCallback
can only be used to memoize callbacks while useMemo
is meant for basically all the other JavaScript entities. Their purpose and usage are basically the same, which make it feel like the different entities that they target is the only difference between the hooks.
But conceptually, it’s possible to mimic the behaviour of memoizing a function through useCallback
using useMemo
too. Although it would look weird, it’s possible to use useMemo
and return a function from the factory function. Theoretically, that would be the same as using useCallback
.
If that holds, it would also mean that there is no real difference between useCallback
and useMemo
except for convenience. So, the question becomes whether there is a difference between memoizing a function through the useCallback
hook, by doing useCallback(() => doSomething())
, and memoizing a function through the useMemo
by returning a callback from the factory function, by doing useMemo(() => () => doSomething())
?
The Deep Dive
In the official React hook docs, there is already a small note that informs us that useCallback(fn, deps)
is equivalent to useMemo(() => fn, deps
. However, this doesn’t provide us with a lot of information or context. Like, does this hold for every type of function, especially inline ones? Are there edge cases? For that, let’s dive into the implementation of both hooks.
The code for both hooks is located inside the react-reconciler
package, specifically in the file with the new React Fiber implementation for hooks. In there, you’ll find the code for all the hooks for when they mount and when they have to be updated. To discover the differences between the two memoization hooks that we’re considering, we have to look at their update functions: updateCallback
and updateMemo
.
Let’s start with the update function of the useCallback
hook:
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
A few things are going on here, but the most important thing is that a subsequent call to the useCallback
hook is handled in the following way:
- The previous dependencies (state) that were used to call this hook are retrieved and compared to the current ones.
- If they’re the same, the hook will return the cached callback from the previous run.
- If they’re not the same, the function will cache the current callback together with the current dependencies inside
hook.memoizedState
. These will then potentially be re-used in subsequent calls.
Now let’s take a look at the update function implementation of useMemo
:
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate(); // <--!!
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
If we compare the two update functions, we notice that they are almost identical. Both hooks go through the same procedure during subsequent calls to the hook. The only difference that we can spot is the value being memoized.
In the case of useCallback
, the value being memoized is directly the first function argument. In the case of useMemo
, the first function argument is also used but in a slightly different way. We can also see that it has a different name, nextCreate
compared to callback
. Instead of memoizing the provided function immediately, the hook invokes it and memoizes the resulting value.
Based on that, we can revisit the original question regarding whether there’s a difference between useCallback(() => doSomething())
and useMemo(() => () => doSomething())
. If we focus on the latter, it means that the hook will invoke the factory function and cache the () => doSomething()
function on mount. As long as the dependencies don’t change, it will keep using the cached version.
This is exactly what useCallback
also does, but without invoking a factory function and taking the defined function instead. Because of how memoization is implemented in React hooks where it only compares to the previous invocation, this means that inline defined functions also work perfectly fine and exactly the same between both hooks.
Conclusion
This article compared two commonly used React hooks for memoization, useCallback
and useMemo
. Both of them stand at the foundation of performant React development. At first glance, both hooks are extremely similar to the extent that one can doubt whether there is any difference. Is there an actual difference or are they wrappers around the same logic with different names for convenience?
To answer this question, this article took a deep dive into the implementations of both hooks. There, we found that the logic behind both hooks is basically the same except for how the memoized value is determined. Considering the way this is implemented and the way memoization in React hooks is implemented, there’s no difference between memoizing an (inline) function normally using useCallback
or returning it from useMemo
.
Based on that, it can be concluded that there is no technical difference between both hooks and that 2 separate hooks only exist for the sake of convenience.