How does the useMemo hook work in React?
Nick Scialli
June 08, 2021
The useMemo
hook in React is actually one of my favorite hooks—it enables a lot of best practices and solves some potentially tricky bugs when writing React components.
How it works
The useMemo
hook is a function that takes two arguments: a callback function and a dependency array:
function MyComponent() {
const myValue = useMemo(
() => {
/* this is the callback function */
},
[
/* this is the dependency array */
]
);
return <>Here's the value: {myValue}!</>;
}
A more realistic version of the interface might look like this:
function MyComponent({ a, b, c }) {
const myValue = useMemo(() => {
return computeSomething(a, b, c);
}, [a, b, c]);
return <>Here's the value: {myValue}!</>;
}
But wait, why not just directly assign computeSomething(a, b, c)
to myValue
? Couldn’t it look like this?
function MyComponent({ a, b, c }) {
const myValue = computeSomething(a, b, c);
return <>Here's the value: {myValue}!</>;
}
The answer, as with many things in programming, is maybe.
What does useMemo solve?
When you remove the useMemo
hook, computeSomething
is run every time the component renders. That might be okay for simple functions, but it could be a performance hit if computeSomething
is expensive. Additionally, this has implications for referential equality, which becomes important when we need some assurance that myValue
is the same as the last time the component was rendered.
Let’s first talk about preventing expensive recomputes and next discuss referential equality.
Preventing expensive recomputes
Let’s say our computeSomething
function is particularly expensive. Maybe it loops through a bunch of elements multiple times. At any rate, computeSomething
is a function of a
, b
, and c
. Assuming it’s a pure function (i.e., it has no side effects and will return the same output for the same inputs), then there should be absolutely no reason to recompute myValue
unless a
, b
, or c
changes. This is exactly what the useMemo
hook does! It will return a memoized value from the provided function unless one of the values in the dependency array changes.
A quick example of avoiding recomputes
In the following code snippet, I have made our component just a little more complex. Now, it has a count
state that we’re able to increment using a button
.
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myValue = useMemo(() => {
return computeSomething(a, b, c);
}, [a, b, c]);
return (
<>
Here's the value: {myValue}!
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
Note that computeSomething
doesn’t rely on the count
variable, so it would make no sense to recompute it when count
changes. But we’re good—with useMemo
, we don’t recompute the value unless something in the dependency array changes.
Referential equality
Lack of referential equality is probably the more frequently encountered issue when it comes to recomputed values. Let’s say we have a function called makeObject
that takes a
, b
, and c
as arguments and returns an object:
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const myObject = makeObject(a, b, c);
return <>Here's the object: {JSON.stringify(myObject)}</>;
}
It’s important to remember that on each render of the component, makeObject
will return a new object with a new reference in memory. React relies on referential equality for quite a bit of rendering logic. Just as a refresher, objects with the same properties are not equal due to lack of referntial equality: they reference two different objects in memory. For example, the following is false.
{a : 1} === {a: 1};
// false
Back to our useMemo
discussion—We’re going to throw our count
button back in here as well as a new component:
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = makeObject(a, b, c);
return (
<>
<SomeComponent obj={myObject} />
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
If we click the Increment
button, we increment our count, we re-run makeObject
(needlessly!), and then we re-render SomeComponent
because myObject
now points to a different object in memory than it did the previous render! This might not be a big deal for a lot of scenarios, but if you’re hitting performance issues, you might want to considering memoizing the makeObject
call.
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = useMemo(() => makeObject(a, b, c), [a, b, c]);
return (
<>
<SomeComponent obj={myObject} />
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
Where this becomes a big deal: dependency arrays
In our previous scenario, we saw that you could hit performance issues from rendering a component too many times. While this is true, it’s often not something you need to worry about until you’re actually hitting performance problems.
What will affect you, however, is referential equality issues within dependency arrays. I see this a lot with useEffect
dependency arrays. Consider the following code:
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = makeObject(a, b, c);
useEffect(() => {
doSomething(myObject);
}, [myObject]);
return (
<>
Here's the object: {JSON.stringify(myObject)}
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
We now have a situation where incrementing the count
will run the useEffect
hook since myObject
will be needlessly recreated as a new object in memory. This could lead to all sorts of undesired results! Depending on what doSomething
is, it could also result in an infinite effect loop, yikes!
Again, the solution to this problem is a useMemo
hook to memoize the return value of our function:
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = useMemo(() => makeObject(a, b, c), [a, b, c]);
useEffect(() => {
doSomething(myObject);
}, [myObject]);
return (
<>
Here's the object: {JSON.stringify(myObject)}
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
Conclusion
I hope this post helped you understand the many reasons to invest in the useMemo
hook!
Nick Scialli is a senior UI engineer at Microsoft.