DEV Community

Chen Asraf
Chen Asraf

Posted on • Edited on • Originally published at casraf.dev

How to use React memoization hooks for increased performance

As React apps grow bigger and more complex, performance becomes more and more of a problem. As components become larger and contain more and more sub-components, rendering becomes slow and turns into a bottleneck.

How do we tackle this? If you haven't used useMemo and useCallback, we can start with those.

In this tutorial, we will take a look at how these 2 very easy and handy callbacks work, and why they are so useful. In fact, these days my eyes get sore when I don't see them used. So let's dive in to what they do.


React.useMemo

This React hook's one goal is to save a value for later use, and not re-calculating it on the spot.

Let's take an example of some expensive logic that runs in our render function:

const ExpensiveComponent: React.FC = (props) => {
  const [list, setList] = React.useState([])
  const [counter, setCounter] = React.useState(0)
  const multiplied = list.map((i) => (i * 972 + 1000) / 5213).join(', ')

  function addRandom() {
    setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
  }

  function increaseCounter() {
    setCounter((prev) => ++prev)
  }

  return (
    <div>
      Counter: {counter}
      <br />
      Multiplied: {multiplied}
      <br />
      <button onClick={addRandom}>Add Random</button>
      <button onClick={increaseCounter}>Increase Counter</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Doesn't seem very problematic, but take a look at the multiplied variable. The logic in this example isn't too bad, but imagine working with a giant list of special objects. This mapping alone could be a performance problem, especially if it's looped in a parent component.

In this case, there is another state hook - counter. When setCounter is called, multiplied will be calculated all over again, wasting previous resources, even when an update in that case is not needed as these variables are independent of each-other.

That's where useMemo comes in hand (read the official docs).

You can use this hook to save the value, and retrieve the same object until a re-calculation is needed.

Here's how it's used, the only line we need to change is the multiplied definition:

const multiplied = React.useMemo(() => {
  console.log('recalculating multiplied:', list)
  return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
}, [list])
Enter fullscreen mode Exit fullscreen mode

The useMemo hook takes 2 arguments:

  1. The create function - used to return the calculated value of the variable we want to eventually use
  2. A list of dependencies. The dependency list is used to determine when a new value should be calculated - i.e, when to run the create function again.

We added a console.log call here just to note when a new value is being calculated.

And with those changes, we can try our component again (here is the updated code just in case):

const ExpensiveComponent: React.FC = (props) => {
  const [list, setList] = React.useState([])
  const [counter, setCounter] = React.useState(0)
  const multiplied = React.useMemo(() => {
    console.log('recalculating multiplied:', list)
    return list.map((i) => (i * 972 + 1000) / 5213).join(', ')
  }, [list])

  function addRandom() {
    setList((prev) => [...prev, Math.floor(Math.random() * 10000)])
  }

  function increaseCounter() {
    setCounter((prev) => ++prev)
  }

  return (
    <div>
      Counter: {counter}
      <br />
      Multiplied: {multiplied}
      <br />
      <button onClick={addRandom}>Add Random</button>
      <button onClick={increaseCounter}>Increase Counter</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

If you now change the counter by using the "Increase Counter" button, you will see our console.log call is not being invoked again until we use the other button for "Add Random".

React.useCallback

Now we have the other hook - useCallback (read the official docs).

This works exactly like the useMemo hook - except it is for functions instead of variable values.

We can take our button functions, and wrap each in this hook to make sure our function reference only changes when needed.

const ExpensiveComponent: React.FC = (props) => {
  const [list, setList] = React.useState([])
  const [counter, setCounter] = React.useState(0)
  const multiplied = React.useMemo(
    () => list.map((i) => (i * 972 + 1000) / 5213).join(', '),
    [list],
  )
  const addRandom = React.useCallback(
    () => setList((prev) => [...prev, Math.floor(Math.random() * 10000)]),
    [setList],
  )
  const increaseCounter = React.useCallback(() => setCounter((prev) => ++prev), [setCounter])

  return (
    <div>
      Counter: {counter}
      <br />
      Multiplied: {multiplied}
      <br />
      <button onClick={addRandom}>Add Random</button>
      <button onClick={increaseCounter}>Increase Counter</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now both our variables and functions are memoized and will only change reference when their dependencies dictate they do.

Caveats

Using these hooks don't come without their share of problems.

  1. Consider whether this actually improves performance or not in your specific case. If your state is pretty regularly changed and these memoizations have to run pretty often, their performance increases might be outweighed by the performance costs of actually running the memoization logic.

  2. Dependency checking and generating can be expensive. Be careful what you put in the dependency lists, and if needed, make some more memoization and map your objects and lists in deterministic ways so that they are statically inspectable easily. Also avoid using expensive methods such as JSON.stringify to create those memoizations or dependencies, as it might also be too expensive to be worth the trouble and might make things worse.

Other things to consider

You might want to make sure your project uses lint rules that enforce exhaustive dependencies, as they make tracking these things much more easy.

In some cases, you might want to add ignore comments in very specific places, but it makes it very clear that this part is built that way intentionally and prevents more confusion about when to update the dependencies.


Hopefully you find this useful. There are many other hooks to learn about, but these 2 are very useful and often ignored, so I thought it would be good to highlight them. If you are interested you might want to look up useRef and how it differs from useMemo, or maybe I'll make another part about it in the future. Who knows?

Top comments (0)