5 Aug 2021
A Visual Guide to React Rendering - useMemo
This is a 3rd chapter of "A Visual Guide to React Rendering". Check out previous chapters: It always re-renders and Props.
A quick quiz:
A Child component is wrapped with memo
. When the user role is "Admin", we want to pass an option to the Child to show a sidebar. However, the Child re-renders even when we change the user name. How do we prevent that?
Should we wrap showSidebar
calculation in useMemo
? Scroll down to see the answer π
Sorry, this was a purposefully misleading question π. Let's simplify the example to see why.
Even if we set the value for showSidebar
directly, the Child still re-renders. That's because the options
prop is an object. And we know from the previous chapter that objects are non-primitive values - they are compared by reference. Therefore, the Child always re-renders because options !== options
, regardless of how we calculate the showSidebar
value. There are two ways to prevent the Child from re-rendering.
1. Flattening props
The showSidebar
stores a primitive value (boolean). So if we pass the showSidebar
prop directly, instead of using the options
object, it will only re-render when the value of this boolean changes.
But sometimes, you do need to pass an object prop. Maybe your architecture requires that, or you use a third-party component and don't have a choice. What to do in this case?
2. useMemo
Remember, the easiest way to provide the same reference for a non-primitive prop is to define its value outside the React component. We did that in the previous chapter.
However, in our case, it's impossible to define options
outside the component since it relies on the component's state. In situations like this, we can utilize useMemo
. The useMemo
hook will cache the result of its calculation, and instead of returning a new value on every render, it will return the old, cached value. For non-primitive values, it will return the same reference.
It works. The options prop receives the cached value from useMemo
, and Child doesn't re-render. But wait, now the options
prop doesn't update even when we update the user role. This happens because we supply an empty list of dependencies as the second argument of useMemo
.
Dependency list
useMemo will only recompute the memoized value when one of the dependencies has changed. This optimization helps to avoid expensive calculations on every render.
Since we supply an empty list of dependencies, useMemo
will not recalculate the value when Parent re-renders. Let's fix this by adding user
to the list of dependencies.
Now we're back where we started. Child re-renders even when user.name
updates, which is an unrelated property for Child. To solve this, we need to understand how useMemo
dependencies work.
On every render, the useMemo
shallowly compares every dependency in the list (prevDependency === dependency
). If any dependency changes, useMemo
recalculates the value and stores the updated version in the cache. In the previous article, we went through the shallow comparison of memo
and how it works with primitive vs non-primitive values in javascript. The same rules apply to useMemo
.
Every state update in our example is immutable. This means that every time we update the name or role of the user, we create a new user object from scratch.
The useMemo
detects that prevUser !== user
and recalculates.
But notice that user.role
holds a primitive value (string). This means we can directly put it in the dependency list and not worry about reference comparison. Only when the value of user.role
changes will useMemo
recalculate.
Performance
In this article, we explored useMemo
as a tool for providing a stable reference for a non-primitive prop. In rare cases, React may choose to forget the memoized value and recalculate useMemo
even if dependencies don't change. But as long as you use it for performance reasons, you'll be fine. Just write your code so that it still works even if useMemo
recalculates. In our example, even if React chooses to recalculate useMemo
, the only unintended consequence is that Child re-renders.
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.
Also, keep in mind that you don't need to fix every unnecessary re-render. Sometimes the performance cost of useMemo may outweigh its benefits. Check out When to useMemo and useCallback by Kent C. Dodds.