Alex Sidorenko

A Visual Guide to React Rendering - useMemo

August 05, 2021

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:

Child 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?

Two components Parent and Child. Parent holds a state {name: 'Alex', role: 'Default'}. Child receives an object prop 'options' with a property 'showSidebar' in it. Parent passes showSidebar = true when user.role is 'Admin'. Child re-renders even when 'user.name' updates

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.

Two components Parent and Child. Parent holds a state {name: 'Alex', role: 'Default'}. Child receives an object prop 'options' with a property 'showSidebar' set to 'true'. Child re-renders even when 'user.name' updates

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 compare by reference. Therefore the Child always re-renders because options !== options, regardless of how we calculate 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.

Two components Parent and Child. Parent holds a state {name: 'Alex', role: 'Default'}. Child receives a prop 'showSidebar'. Parent passes showSidebar = true when user.role is 'Admin'. Child doesn't re-render when 'user.name' updates

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 state of this component. In situations like that, we can utilize useMemo. The useMemo 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.

Two components Parent and Child. Parent holds a state {name: 'Alex', role: 'Default'}. Child receives an object prop 'options' with a property 'showSidebar' in it. Parent memoizes result of 'options' with useMemo with no dependencies. Parent passes showSidebar = true when user.role is 'Admin'. Child never re-renders

It works. The options prop receives 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. It 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.

Two components Parent and Child. Parent holds a state {name: 'Alex', role: 'Default'}. Child receives an object prop 'options' with a property 'showSidebar' in it. Parent memoizes result of 'options' with useMemo with 'user' as the only dependency. Parent passes showSidebar = true when user.role is 'Admin'. Child re-renders even when 'user.name' updates

Now we are back when we started. Child re-renders even when user.name updates, which is an unrelated property for Child. To solve that 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. It means that every time we update the name or role of the user, we create the 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). It means we can directly put it in the dependency list and don’t worry about reference comparison. Only when the value of user.role changes, useMemo recalculate.

Two components Parent and Child. Parent holds a state {name: 'Alex', role: 'Default'}. Child receives an object prop 'options' with a property 'showSidebar' in it. Parent memoizes result of 'options' with useMemo with 'user.role' as the only dependency. Parent passes showSidebar = true when user.role is 'Admin'. Child doesn't re-render when 'user.name' updates

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 will 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 unintentional thing that happens is Child re-render.

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 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

Want to get better at modern React?

Subscribe to get one short article delivered to your inbox every week

One article a week. No spam.
Unsubscribe any time