A common misconception with useMemo and useCallback
If you've been programming with React for a while, you have already used useCallback
and useMemo
hooks. A common misconception with useCallback
and useMemo
is that their purpose is to preserve the identity of values across renders. However, their goal is to optimize the performance of React applications.
The difference might seem meaningless since performance will be optimized by recomputing values only if a dependency changes. This makes us think that the identity of the memoized value should be preserved. And it will, but not always.
If we take a look at React's documentation on useCallback
and useMemo
, we can find the following:
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.
If by reading the documentation, you're still not sure if when using useCallback
you should have the same considerations as when using useMemo
, you can check this issue on React's repository.
In short, what the documentation tells us, is that we can't rely on useMemo
and useCallback
as a means to preserve identity, only as a performance optimization that might recompute the memoized value when it sees fit.
When might this misconception be a problem
Whenever we reach for useMemo
or useCallback
and preserving the identity across renders of a value or callback is crucial for our app to work correctly, we are susceptible to bugs.
A concrete example is when using a debounced function. When using a debounced function, we need to make sure that we're preserving its identity. Otherwise, it won't work as expected.
Let's imagine a use-case where we want to perform a search only after the user stops typing. For that, we are going to debounce the function that will trigger the API request, and have it called every time the user types something.
function fetchHits(query, dispatch){ /* Makes API call*/ }
function App() {
...
const debouncedFetchHits = debounce(query => fetchHits(query, dispatch), 500)
return <>...</>
}
Due to the nature of React function components, we know that the identity debouncedFetchHits
won't be preserved across renders. So our implementation is broken. To fix that, we'll wrap the debounced function in a useCallback
.
function fetchHits(query, dispatch){ /* Makes API call*/ }
function App() {
...
const debouncedFetchHits = useCallback(
debounce(query => fetchHits(query, dispatch), 500),
[])
// NOTE: fetchHits is on outer scope, so it shouldn't be in the dependencies []
return <>...</>
}
If we test this solution, it will seem to work. However, it might not work all the time since the identity of debouncedFetchHits
might not always be the same across renders. In our use-case, that would mean that, occasionally, we would be making API requests for queries that aren't relevant to the user, and showing him results that are not of his interest.
Note: If you'd like to know how to fully implement the perform a search only after the user stops typing use-case, you might like this article.
Solution
The solution is to use useRef
hook instead of the useCallback
.
function fetchHits(query, dispatch){ /* Makes API call*/ }
function App() {
...
const debouncedFetchHits = useRef(
debounce(query => fetchHits(query, dispatch), 500)
).current
return <>...</>
}
The useRef
hook gives us the semantic guarantee of identity across renders that useCallback
and useMemo
don't. Thus, we no longer need to worry about hard to track bugs caused by changes to the identity of debouncedFetchHits
.
Conclusion
Next time you reach out to useMemo
or useCallback
, ask yourself if you want to do a performance optimization or if you want a guarantee of identity. If what you're looking for is a performance optimization, useMemo
and useCallback
are the way to go. Otherwise, useRef
is what you're looking for.