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.