The useMemo Hook
In the last lesson, we saw how the React.memo
helper lets us memoize a component, so that it only re-renders when its props/state changes.
In this lesson, we're going to learn about another tool that lets us do a different sort of memoization: the useMemo
hook.
The fundamental idea with useMemo
is that it allows us to “remember” a computed value between renders.
We generally use this hook for performance optimizations. It can be used in two separate-but-related ways:
- We can reduce the amount of work that needs to be done in a given render.
- We can reduce the number of times that a component is re-rendered.
Let's talk about these strategies, one at a time.
Use case 1: Heavy computations
Let's suppose we're building a tool to help users find all of the prime numbers between 0 and selectedNum
, where selectedNum
is a user-supplied value. A prime number is a number that can only be divided by 1 and itself, like 17.
Here's one possible implementation. Try changing "Your number" to see how it works:
Code Playground
In this code, we have a single piece of state, selectedNum
. Using a for loop, we manually calculate all of the prime numbers between 0 and selectedNum
. The user can change selectedNum
by editing a controlled number input.
This code requires a significant amount of computation. If the user picks a large selectedNum
, we'll need to go through tens of thousands of numbers, checking if each one is prime. And, while there are more efficient prime-checking algorithms than the one I used above, it's always going to be computationally intensive.
Now, we can't avoid this work altogether. We need to do all this work at least once, and again whenever the user picks a new number. But if we wind up doing this work gratuitously, we can run into performance problems.
For example, let's suppose our example also features a digital clock, using the useTime hook we created.
Code Playground
Our application now has two pieces of state, selectedNum
and time
. Once per second, the time
variable is updated to reflect the current time, and that value is used to render a digital clock in the top-right corner.
Here's the issue: whenever either of these state variables change, the component re-renders, and we re-run all of these expensive computations. And because time
changes once per second, it means we're constantly re-generating that list of primes, even when the user's selected number hasn't changed!
In JavaScript, we only have one main thread, and we're keeping it super busy by running this code over and over, every single second. It means that the application might feel sluggish as the user tries to do other things, especially on lower-end devices.
But what if we could “skip” these calculations? If we already have the list of primes for a given number, why not re-use that value instead of calculating it from scratch every time?
This is precisely what useMemo
allows us to do. Here's what it looks like:
const allPrimes = React.useMemo(() => { const result = [];
for (let counter = 2; counter < selectedNum; counter++) { if (isPrime(counter)) { result.push(counter); } }
return result;}, [selectedNum]);
useMemo
takes two arguments:
- A chunk of work to be performed, wrapped up in a callback function
- A list of dependencies
During mount, when this component is rendered for the very first time, React will invoke this function to run all of this logic, calculating all of the primes. Whatever we return from this function is assigned to the allPrimes
variable.
For every subsequent render, however, React has a choice to make. Should it:
- Invoke the function again, to re-calculate the value, or
- Re-use the data it already has, from the last time it did this work.
To answer this question, React looks at the supplied list of dependencies. Have any of them changed since the previous render? If so, React will rerun the supplied function, to calculate a new value. Otherwise, it'll skip all that work and reuse the previously-calculated value.
useMemo
is essentially like a lil’ cache, and the dependencies are the cache invalidation strategy.
In this case, we're essentially saying “recalculate the list of primes only when selectedNum
changes”. When the component re-renders for other reasons (eg. the time
state variable changing), useMemo
ignores the function and passes along the cached value.
Here's the live version of our solution, implementing the useMemo
hook:
Code Playground
Use case 2: Preserved references
So we've seen how useMemo
can help improve performance by caching expensive calculations. This is one of the ways that this hook can be used, but it's not the only way! Let's talk about the other use case.
In the example below, I've created a Boxes
component. It displays a set of colorful boxes, to be used for some sort of decorative purpose.
I also have a bit of unrelated state, the user's name.
Code Playground
Our Boxes
component has been made pure by React.memo()
. This means that it should only re-render whenever its props change.
And yet, whenever the user changes their name, PureBoxes
re-renders as well!
Here's a graph showing this dynamic. Try typing in the text input, and notice how both components re-render:
App
Boxes
Props: { boxes }
Pure Component
What the heck?! Why isn't our React.memo()
force field protecting us here??
The PureBoxes
component only has 1 prop, boxes
, and it appears as though we're giving it the exact same data on every render. It's always the same thing: a red box, a wide purple box, a yellow box. We do have a boxWidth
state variable that affects the boxes
array, but we aren't changing it!
Here's the problem: every time React re-renders, we're producing a brand new array. They're equivalent in terms of value, but not in terms of reference.
I think it'll be helpful if we forget about React for a second, and talk about plain old JavaScript. Let's look at a similar situation:
function getNumbers() { return [1, 2, 3];}
const firstResult = getNumbers();const secondResult = getNumbers();
console.log(firstResult === secondResult);
What do you think? Is firstResult
equal to secondResult
?
In a sense, they are. Both variables hold an identical structure, [1, 2, 3]
. But that's not what the ===
operator is actually checking.
Instead, ===
is checking whether two expressions are the same thing.
This is something we talked about in the “Immutability Revisited” lesson. When it comes to objects and arrays, it's not enough for them to look the same. They have to be the same. Both variables need to point to the same entity held in the computer's memory.
Every time we invoke the getNumbers
function, we create a brand-new array, a distinct thing held in the computer's memory. If we invoke it multiple times, we'll store multiple copies of this array in-memory.
Note that simple data types — things like strings, numbers, and boolean values — can be compared by value. But when it comes to arrays and objects, they're only compared by reference. For more information on this distinction, check out this wonderful blog post by Dave Ceddia: A Visual Guide to References in JavaScript.
Taking this back to React: Our PureBoxes
React component is also a JavaScript function. When we render it, we invoke that function:
// Every time we render this component, we call this function...function App() { // ...and wind up creating a brand new array... const boxes = [ { flex: boxWidth, background: 'hsl(345deg 100% 50%)' }, { flex: 3, background: 'hsl(260deg 100% 40%)' }, { flex: 1, background: 'hsl(50deg 100% 60%)' }, ];
// ...which is then passed as a prop to this component! return ( <PureBoxes boxes={boxes} /> );}
When the name
state changes, our App
component re-renders, which re-runs all of the code. We construct a brand-new boxes
array, and pass it onto our PureBoxes
component.
And PureBoxes
re-renders, because we gave it a brand new array!
The structure of the boxes
array hasn't changed between renders, but that isn't relevant. All React knows is that the boxes
prop has received a freshly-created, never-before-seen array.
To solve this problem, we can use the useMemo
hook:
const boxes = React.useMemo(() => { return [ { flex: boxWidth, background: 'hsl(345deg 100% 50%)' }, { flex: 3, background: 'hsl(260deg 100% 40%)' }, { flex: 1, background: 'hsl(50deg 100% 60%)' }, ];}, [boxWidth]);
Unlike the example we saw earlier, with the prime numbers, we're not worried about a computationally-expensive calculation here. Our only goal is to preserve a reference to a particular array.
We list boxWidth
as a dependency, because we do want the PureBoxes
component to re-render when the user tweaks the width of the red box.
I think a quick sketch will help illustrate. Before, we were creating a brand new array, as part of each snapshot:
With useMemo
, however, we're re-using a previously-created boxes
array:
By preserving the same reference across multiple renders, we allow pure components to function the way we want them to, ignoring renders that don't affect the UI.
Here's an updated sandbox, including the useMemo
fix. Try typing in the “Name” field, and keep an eye on the console:
Code Playground
- Render Boxes
- Render Boxes