Avatar

Nǐ hǎo, I'm Julia.

Deeper Dive Into React useMemo

#react

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 state onChange. This in turn causes the results function to re-calculate, where results 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! 😜

© 2016-2024 Julia Tan · Powered by Next JS.