Alex Sidorenko AvatarAlex Sidorenko

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 πŸ‘‡

It's a trap meme

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.

React docs - useMemo

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.

Comparison rules of useMemo dependency list

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.

const updateName = (name) => setUser({ ...user, name: name });

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.

React docs - useMemo

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.

Next chapter

A Visual Giude to React Rendering - useCallback


Follow me on 𝕏 for short videos about Next.js

𝕏 Follow for more