Deeper Dive Into React useMemo
5 min read
If you're new here, be sure to first check out my posts on the differences between React.memo and useMemo, and a deeper dive into React.memo. This post completes the last in the series and talks about the useMemo
hook and when / when not to use it.
When to use useMemo
Use Case 1: Stopping computationally expensive, unnecessary re-renders
Let's go back to the example I had in my first post. This illustrates the use case where you have a function that keeps re-rendering, because the state of its parent component keeps changing.
export type VideoGameSearchProps = {
allGames: VideoGameProps[],
}
export const VideoGameSearch: React.FC<VideoGameSearchProps> = ({ allGames }) => {
const [searchTerm, setSearchTerm] = React.useState('')
const [count, setCount] = React.useState < number > 1
// NOTE useMemo here!!
const results = useMemo(() => {
console.log('Filtering games')
return allGames.filter((game) => game.name.includes(searchTerm))
}, [searchTerm, allGames])
const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value)
}
const onClickHandler = () => {
setCount((prevCount) => prevCount + 1)
}
return (
<>
<input type="text" value={searchTerm} onChange={onChangeHandler} />
{results.map((game) => (
<VideoGame key={game.name} rating={game.rating} name={game.name} releaseDate={game.releaseDate} />
))}
<br />
<br />
<p>Count: {count}</p>
<button onClick={onClickHandler}>Increment count</button>
</>
)
}
This is a completely made-up example which would likely never exist in production code, but I wanted to illustrate the takeaway points clearly. In this case, there are 2 things going on in this component:
- A user can click on an "increment count" button which updates the
count
state and displays the current number in the UI. - A user can enter a search query in the input field which updates the
searchTerm
stateonChange
. This in turn causes theresults
function to re-calculate, whereresults
is rendered as a list in the UI.
The incrementing of count
has nothing to do with how searchTerm
is set, or results
run. However, every time count
is incremented, the component re-renders and runs the results
function. It's probably not going to be a big deal here, but what if the allGames
array actually contains millions of elements... and instead of a simple filter function, it was a much more computationally complex calculation? This is where useMemo
would come in handy.
Wrapping the results
function with useMemo
(with searchTerm
and allGames
as dependencies) tells React to only re-run this function, if either of those 2 variables changes. This means that changes in count
would no longer cause results
to be recalculated, with the memoised result being returned instead.
Note: I've added the console.log
in there so you can test it for yourselves to see how many times that function runs with and without the useMemo
when you increment count
!
Use Case 2: Ensuring referential equality when dealing with dependency lists
If you have a case whereby you're relying on a dependency list, e.g. when using a useEffect
hook, you really want to ensure you're only updating the component when the dependency values have truly changed.
useEffect(() => {
const gameData = { name, publisher, genres }
thisIsAFunction(gameData)
}, [name, publisher, genres])
In this example, assuming name
, publisher
and genres
are all strings, you shouldn't have a problem. React does a referential equality check on gameData
to decide whether the component should be updated, and because gameData
only comprises strings (i.e. primitives), this will work as we expect.
To illustrate the point, we wouldn't want to have this for example, because gameData
will be a new instance every time React runs the useEffect
check, which means re-running thisIsAFunction
every time because in Javascript-land, gameData
has changed.
const gameData = { name, publisher, genres }
useEffect(() => {
thisIsAFunction(gameData)
}, [name, publisher, genres])
So back to this - all good right?
useEffect(() => {
const gameData = { name, publisher, genres }
thisIsAFunction(gameData)
}, [name, publisher, genres])
Unfortunately not, because we run into a similar problem if one of name
, publisher
or genres
is a non-primitive. Let's say instead of a string, genres
is actually an array of strings. In Javascript, arrays are non-primitives which means [] === []
results in false
.
So to expand out the example, we've got something like this:
const GamesComponent = ({ name, publisher, genres }) => {
const thisIsAFunction = (
gameData, // ...
) =>
useEffect(() => {
const gameData = { name, publisher, genres }
thisIsAFunction(gameData)
}, [name, publisher, genres])
return //...
}
const ParentGamesComponent = () => {
const name = 'God of War'
const publisher = 'Sony'
const genres = ['action-adventure', 'platform']
return <GamesComponent name={name} publisher={publisher} genres={genres} />
}
In this case, despite genres
in effect being a constant array of strings, Javascript treats this as a new instance every time it's passed in as a prop when GamesComponent
is re-rendered. useEffect
will thus treat the referential equality check as false and update the component, which is not what we want. ๐ข
This is where useMemo
comes in handy. The empty []
effectively tells React not to update genres
after mounting.
const ParentGamesComponent = () => {
const name = 'God of War'
const publisher = 'Sony'
const genres = useMemo(() => ['action-adventure', 'platform'], [])
return <GamesComponent name={name} publisher={publisher} genres={genres} />
}
Side note: if one of the props is a callback function (i.e. not a primitive), use the useCallback
hook to achieve the same effect.
When not to use useMemo
Alright, so if not already clear by now after 3 posts, let me reiterate that React is smart and speedy in its own right. So, unless you're experiencing "use case 2" above, or perhaps "use case 1" with a noticeable lag or quantifiable performance dip, err on the side of not using useMemo
! ๐