In this article, we will explore when and how to use React's useMemo
Hook to increase your app's performance.
TL;DR
If you’re here solely to understand when to (and not to) use useMemeo
and don’t want to work through the examples, I’ll spare you. According to the React Docs, much like useCallback
, useMemo
is an opt-in performance enhancer Hook used to create referential equality for function results. The three most common use cases are:
- if the calculation causes noticeable slowdown in other parts of your app
- if you’re passing the calculation as a prop to a child component
- if the calculation is used as a dependency in a Hook
If none of those conditions is being met, you’re likely wasting time, lines, and sacrificing readability (and memory) by proactively “optimizing” your app.
With that out of the way, Let’s get started.
Getting Started
Buckle up and strap in. This article is on the heavier side for both theory and length.
If you'd like to follow along in your local IDE, you can find the GitHub Repo here. Otherwise, you can reference the code snippets, though you will miss out on the performance comparisons.
fork and clone
cd client
npm i
npm start
Starter Code
We'll begin with a quick overview of our starter code. In App.js
, you'll find a function named "jacobsthal
," two pieces of state, and a variable named “calculation
”. Notice we wrapped jacobsthal
in a useCallback
Hook, and calculation
is the returned value from calling jacobsthal
.
The JSX renders both inputs and their respective values. If you need a refresher on what service the useCallback
Hook provides, I'd suggest you pause here and give my useCallback
article a quick read.
Our
jacobsthal
function is a simple, recursive function that returns the Jacobsthal Number at a given index. The specifics of the code and Jacobsthal Number don't matter foruseMemo
. All we care about is that it's defined within our component, hence the implementation ofuseCallback
, and that it’s computationally expensive.
If we provide a small value to the number
input, our React app behaves as expected, snappily rendering the result. However, as we increase the value of our input, while the app still provides the desired output, it takes increasingly longer to render.
Why is This Happening?
Thirty-five will be our test case because it is slow enough to be annoying but still testable. So we’ll type 35 and wait for our output to calculate.
Now start typing into the second input. See how slow it is to render? That’s because when the input changed, the entire component re-rendered, and our expensive function recalculated the output before re-rendering, even though our jacobsthal
output didn’t change.
This is obviously a problem.
A Quick Aside:
Junior Developers make this mistake far too often. You don’t always need
useState
for your forms. I'll even propose that you usually don't. If no part of your application needs to see the real-time value, like when submitting a form to an API, you should be usinguseRef
instead. We will get into how and why in a later article.
So with the stage set and the curtains drawn, how do we resolve the issue at hand?
Memoization
Memoization is a Programming technique that stores the results of a function call, so the next time you call that function, it doesn’t have to recalculate the output. Instead, it can return the stored result, saving time complexity with recursive functions.
That’s all you need to know for now, but if you’d like a more in-depth explanation, check out this Memoization in JavaScript article by GeeksforGeeks. And at the end of this article, we will refactor our jacobsthal
function to implement proper JavaScript memoization.
useMemo
vs. useCallback
To sum it up, useCallback
stores the definition of the function, so it doesn't unnecessarily redefine on every render. useCallback
creates referential equality between instances of the function across renders.
Similarly, useMemo
stores the result of the function call, so it doesn't unnecessarily recalculate on every render. useMemo
creates referential equality between instances of the value across renders.
You can already see how this is helpful and leads directly to the primary purpose of useMemo
. In short, the aptly named useMemo
Hook is React’s built-in memoization tool.
useMemo
in Action
Let’s write some code!
We’ll start by importing useMemo
from 'react'
.
useMemo
Syntax
You guessed it, useMemo
has a similar syntactical skeleton to both useEffect
and useCallback
: an anonymous callback with a dependency array.
As in our useCallback
example, we want to cache what's returned from this Hook. So we will assign our calculation
variable to the return value of useMemo
, wrapping our function call in an anonymous callback.
Remember to return
the result of your function call so it is accessible by useMemo
!
After we save, we’ll notice a familiar warning from React. Our Hook is missing a dependency.
Before blindly obeying React’s warnings, let’s first think through the purpose of this dependency array and the functionality it extends to our application.
The intent of dependency arrays with React Hooks is to trigger our Hook more intentionally and specifically. When the value of the variable being “tracked” changes, the Hook knows it's time to do its thing.
In our specific case, when number
changes, we want our jacobsthal
function to recalculate the result.
So let’s add number
to our dependency array.
Now that we’ve memoized our function, let’s test it out. We’ll start by inputting 35. Our calculation still takes time because our jacobsthal
function is still computationally expensive. But now, when we type in the second input, our React app is again snappy and responsive. It's no longer recalculating our jacobsthal
output because number
has not changed.
Conclusion (kind of)
Because we memoized the results of our function, we’ve created referential equality and eliminated any unnecessary renders, making our React app more performant.
If you only came here for the React piece, thanks so much for reading, and look out for the useRef
article next!
But what to do about our computationally expensive jacobsthal
function? Time to refactor.
We begin by creating a previousValues
parameter with a default value of an empty array. This will be our cache that we will later pass to our recursive sequence. Doing so will spare our recursive sequence from working overtime.
Next, inside our code block, we’ll create a results variable. We will later reassign the value, so we’ll need to use our let
keyword.
Instead of returning our computations directly, we’ll explicitly wrap our recursive sequence in an else
block and assign our return
options to result
.
Now, after our conditionals have evaluated and assigned a value to result
, we set previousValues
at index n
equal to our current result, then return result
, thus caching this value and making it accessible as a return
.
Next, the first thing our function should do is check to see if previousValues
at index n
exists. If it does, we return
it.
Lastly, we’ll pass previousValues
as an argument to our recursive sequence.
Conclusions (for real this time)
Whew. We can now test our newly (and thoroughly) memoized component. Try 35 again. Pretty snappy, huh? So snappy, in fact, that if we enter 1026 as our input, it’s still responsive. Even calculating ‘Infinity
' doesn't crash our app. And yes, useMemo
is still doing its thing.
There is no lag in our other input.
If you’d like to dive deeper with useMemo, you can learn more in the official React docs.
I’m always looking for new friends and colleagues. If you found this article helpful and would like to connect, you can find me at any of my homes on the web.