| Hooks / React / Typescript

useMemo with dependencies — a simple case study

When React 16.8 came out, it completely changed how we write our components. State, lifecycle, refs are now managed with hooks, and in turn the code you write became more comprehensive, and less prone to issues.

Let me start off by stating that this post is not meant to be a thorough explanation of hooks, or useMemo, though I hope it gives you a better understanding them.

UseMemo

useMemo is a React hook, that takes a function, and returns a memoized value. Essentially, it’s caching the values the function returns after its initial execution, which makes it your go-to tool for performance optimization.

But wait, don’t we already have React.Memo for this?

Kind of…

Unlike React.Memo, which is a higher order component and meant to wrap other components, useMemo memoizes function values.

…and since class components and stateless functional components merged into Functional Components, that are essentially functions themselves, useMemo is a perfect fit for caching our components going forward.

useMemo is also more general, in the sense that it takes an array of dependencies, meaning that the memoized value will only get recomputed if one of the specified dependencies change.

In order to understand its workings better, let’s take a look at an example of an issue I was facing before:

ShoppingCart

We have a list of products, all ready to go into a shopping cart, wrapped in useMemo:

const ProductList: React.FC<{
  productItems: string[]
  handleAddToCart: (productId: number) => void
}> = ({ productItems, handleAddToCart }) => {
  const classes = useStyles()
  return useMemo(
    () => (
      <List
        className={classes.root}
        subheader={
          <ListSubheader component="div" id="nested-list-subheader">
            Available Products
          </ListSubheader>
        }
        disablePadding
      >
        {productItems &&
          productItems.map((product, index) => (
            <ProductItem
              key={`productItems.${index}`}
              productId={index}
              product={product}
              handleAddToCart={handleAddToCart}
            />
          ))}
      </List>
    ),
    [productItems, handleAddToCart, classes],
  )
}

When we do add to the cart though, the ProductList component re-renders. That is not what we expected to see, our product list did not change at all.

Looking at it with the handy tool why-did-you-re-render, we immediately see the culprit:

ReRender

Since the shopping cart updates the state on the parent component, it causes it to re-render, which recreates the handleAddToCart function. Since handleAddToCart is a dependency on our useMemo in ProductList, it discards the memoized component value.

How do we fix this?

There are a number of approaches we can take:

  • Refactor!

If this was your first thought, congratulations for having the right mindset :)

Although keep in mind that this is just an example project. In the real world, there are times when you need a quick solution.

  • Can’t we just remove handleAddToCart from the dependencies?

That won’t solve our issue. Remember, the dependency list is there to specify what gets shallow compared for memoization, not what gets passed down.

Let’s see what happens if we do go down that path. We’ll remove the dependency.

That ain’t right. What’s happening?

Since we removed handleAddToCart from the dependencies, when our parent component re-rendered, the function was recreated. However the ProductList component still held the memoized function.

What we ended up with essentially, is two different functions with the same name on our component. React even warns us:

Warning

It’s as if warnings should be taken seriously ;)

Finding a solution

There are multiple ways we can resolve this. One quick and easy solution is to leverage another hook, useCallback.

useCallback works much in the same way as useMemo. The difference is that while useMemo returns a memoized value, useCallback returns a memoized function, which is just what we need.

On our parent component, let’s wrap our handleAddToCart in a useCallback:

const handleAddToCartCallback = useCallback(
  (productId: number) =>
    setShoppingCart(shoppingCart => [...shoppingCart, productItems[productId]]),
  [productItems],
)

No re-renders this time!

The useReducer approach

There is another neat trick you can pull, especially if you either have complex state in your parent component, or you’re passing down multiple functions to manipulate said state.

If you define your state using useReducer, you could pass down its dispatch function to your child components, like so:

Dispatch 1

The dispatch callback won’t get recreated on state changes, which saves us from the need to wrap multiple functions in useCallbacks.

Dispatch 2

Invoking our dispatch…

Success

…and green all around.

That’s it, I hope you had as much fun reading as I did writing it.

Akos Radler

Akos Radler

Full Stack .Net / React developer

Read More