Avatar

Salut, I'm Julia.

React.memo and useMemo - What's the Difference?

#hooks #react #typescript

6 min read

Since first learning React and then building production-ready apps with it, I've been somewhat confused about when it's appropriate to use the memoization wrappers and hooks. The main ones are:

  • React.memo
  • useMemo
  • useCallback

Speaking to other engineers about this, I've gotten responses all the way from "just use it wherever possible - there's no downside", to "not sure...but it's a good idea to use it when rendering lists". None of these answers were satisfactory, so I set aside some time to figure this out once and for all.

Things I'll cover in the series (which I'll break up into separate blog posts for digestibility):

React.memo vs. useMemo - What's the difference?

React.memo

React.memo is a higher order component that's used to wrap a React functional component. The way it works is:

  • React does an initial render of the component when it first loads and stores it in memory.
  • React does a shallow comparison of prop values. If true, React uses the memoized component and skips a re-render. If false, React re-renders the component.

A contrived example looks like this:

export type VideoGameProps = {
  rating: string,
  name: string,
  releaseDate: string,
}

// NOTE React.memo wraps the entire component
export const VideoGame: React.FC<VideoGameProps> = React.memo(({ rating, name, releaseDate }) => (
  <div>
    <p>Name: {name}</p>
    <p>Rating: {rating}</p>
    <p>Release date: {releaseDate}</p>
    <hr />
  </div>
))

Tip 1: You can pass a second argument in to define a stricter comparison function, instead of using the default shallow comparison.

const checkStrictEquality = (prevProps, newProps) => {
  // blah blah
}

React.memo(Component, checkStrictEquality)

Tip 2: Because of the use of shallow comparison, be careful about passing in non-primitive props like an object, array or function. Do not pass these in directly as props, but instead, instantiate and assign these to variables which are then passed in. For functions, the useCallback hook is handy for ensuring the same instance of the function is passed in as props, thus allowing the shallow prop comparison to result in true. For objects and arrays, the useMemo hook might be helpful, which I'll go through in the next section.

const onClickHandler = useCallback(() => {
  // blah blah
}, [insert dependencies here]);

export const VideoGame: React.FC<VideoGameProps> = React.memo(
  ({ rating, name, releaseDate, onClickHandler }) => (
    <div>
      <p>Name: {name}</p>
      <p>Rating: {rating}</p>
      <p>Release date: {releaseDate}</p>
      <button onClick={onClickHandler}>Click</button>
      <hr />
    </div>
  )
);

Tip 3: If you're working with class-based components, think about using a PureComponent instead. PureComponents allow you to define shouldComponentUpdate() which does a shallow comparison of props and state.

useMemo

useMemo is a React hook that can be used to wrap a function or object, within a React component. Similarly to React.memo, the idea is that the function will be run once and the value memoized. This hook takes 2 arguments, the computational function, and an array of dependencies that the function depends on. React will only recompute the memoized value if the value of one of the dependencies changes.

A contrived example:

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>
    </>
  )
}

There's quite a lot going on in the example above, but if you just focus on the const results function, the computed value is memoized and stored, and only recalculated if either the searchTerm or allGames array changes. If we had not used the useMemo hook, the results function would have been constantly recalculated every time we clicked on the button to increment the count state, even though this does not directly affect the results. This is because a state change causes the VideoGameSearch parent component to rerender, thus forcing a recalculation of results.

An alternative approach

If you find yourself having to use the useMemo hook frequently, think about whether there are better ways of composing your app so that re-renders are not triggered for sub-components when they don't need to be. In the example above, I could have created a new component that included the input field and results mapping, and created a second component to display the count and button. All state would then be pushed down into the relevant child components. VideoGameSearch would then look like this:

export const VideoGameSearch: React.FC<VideoGameSearchProps> = ({ allGames }) => {
  return (
    <>
      <VideoGameSearcher allGames={allGames} />
      <Counter />
    </>
  )
}

Since both components are now managing their own state, each will only re-render when their state changes, and not when the other component's state changes. This makes sense in this case, because the counter and search really had nothing to do with one another.

Reintroducing React.memo

So, what if we find ourselves in a situation where we can't push state down completely into child components? Using the same example above, imagine that for whatever reason, the counter state has to be stored at the VideoGameSearch level. In this case, we can use React.memo to wrap the VideoGameSearcher component. This will ensure that the VideoGameSearcher component only re-renders when the allGames prop changes.

Note that we'll need to preserve referential equality, so we'll need to use the useMemo hook to wrap the allGames prop as it will be an array or object. 🙃

export const VideoGameSearch: React.FC<VideoGameSearchProps> = ({ allGames }) => {
  const [count, setCount] = React.useState < number > 1

  const allGamesMemo = useMemo(() => allGames, [allGames])

  return (
    <>
      <VideoGameSearcher allGames={allGamesMemo} /> // Wrapped in React.memo
      <p>Count: {count}</p>
      <button onClick={onClickHandler}>Increment count</button>
    </>
  )
}
export const VideoGameSearcher: React.FC<VideoGameSearchProps> = React.memo(({ allGames }) => {
  // etc.
})

Concluding notes

The reason I say these are contrived examples are because... they are. I made these examples up for the purposes of illustration, and without more context, it's difficult to say whether the use of React.memo or useMemo would be worth it here. The reason for this is that React is already super speedy in how it performs its re-renders. If you're going to intercept this process, and introduce complexity, you should be sure that the tradeoff is indeed worth it.

In the next blog post, I'll go into more detail on when it might make sense to use the React.memo wrapper.

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