Memoization in react

Memoization in react

Why components re-render, pure components, memo HOC and the useMemo and useCallback hooks

Memoization in React

As I mentioned in this article about memoization in Javascript, in programming, memoization is an optimization technique that makes applications more efficient and hence faster by storing computation results in cache, and retrieving that same information from cache the next time it's needed instead of computing it again.

In React, we can optimize our application by avoiding unnecessary components re-render using memoization. As I mentioned too in this other article about managing state in React, components re-render because of two things, a change in state or a change in props, and this is precisely the information we can "cache" to avoid unnecessary re-renders.

Pure components

As John Kagga defines here, "Components are the building blocks of any React app... Simply put, a component is a JavaScript class or function that optionally accepts inputs (props) and returns a React element that describes how a section of the UI should appear."

As mentioned, react supports either class or functional components. A functional component is a plain JavaScript function that returns JSX, and a class component is a JavaScript class that extends React.Component and returns JSX inside a render method.

And what is a pure component then? Well, based on the concept of purity in functional programming paradigms and as I talked about in this article about functions in javascript, a function is said to be pure if:

  • Its return value is only determined by its input values
  • Its return value is always the same for the same input values

In the same way, a React component is considered pure if it renders the same output for the same state and props.

A functional pure component, could look like this:

// Pure component
export default function PureComponent({name, lastName}) {
  return (
    <div>My name is {name} {lastName}</div>
  )
}

See that we pass two props, and the component renders those two props. If the props are the same the render will always be the same.

On the other side, say for example we add a random number to each prop before rendering. Then the output might be different even if the props remain the same, so this would be an impure component.

// Impure component
export default function ImpurePureComponent({name, lastName}) {
  return (
    <div>My "impure" name is {name + Math.random()} {lastName + Math.random()}</div>
  )
}

Same examples with class components would be:

// Pure component
class PureComponent extends React.Component {
    render() {
      return (
        <div>My "name is {this.props.name} {this.props.lastName}</div>
      )
    }
  }

export default PureComponent
// Impure component
class ImpurePureComponent extends React.Component {
    render() {
      return (
        <div>My "impure" name is {this.props.name + Math.random()} {this.props.lastName + Math.random()}</div>
      )
    }
  }

export default ImpurePureComponent

For class pure components, React provides the PureComponent base class. Class components that extend the React.PureComponent class have some performance improvements and render optimizations since React implements the shouldComponentUpdate() method for them with a shallow comparison for props and state.

Let's see it in an example. Here we have a class component that is a counter, with buttons to change that counter adding or subtracting numbers. We also have a child component to which we're passing a prop name which is a string.

import React from "react"
import Child from "./child"

class Counter extends React.Component {
    constructor(props) {
      super(props)
      this.state = { count: 0 }
    }

    handleIncrement = () => { this.setState(prevState => {
        return { count: prevState.count - 1 };
      })
    }

    handleDecrement = () => { this.setState(prevState => {
        return { count: prevState.count + 1 };
      })
    }

    render() {
      console.log("Parent render")

      return (
        <div className="App">

          <button onClick={this.handleIncrement}>Increment</button>
          <button onClick={this.handleDecrement}>Decrement</button>

          <h2>{this.state.count}</h2>

          <Child name={"Skinny Jack"} />
        </div>
      )
    }
  }

  export default Counter

The child component is a pure component that just renders the received prop.

import React from "react"

class Child extends React.Component {
    render() {
      console.log("Skinny Jack")
      return (
          <h2>{this.props.name}</h2>
      )
    }
  }

export default Child

Notice that we've added console.logs to both components so that we've get console messages each time they render. And talking about that, guess what happens when we press the increment or decrement buttons? Our console will look like this:

image.png

The child component is re-rendering even if it's receiving always the same prop.

To implement memoization and optimize this situation, we need extend the React.PureComponent class in our child component, like this:

import React from "react"

class Child extends React.PureComponent {
    render() {
      console.log("Skinny Jack")
      return (
          <h2>{this.props.name}</h2>
      )
    }
  }

export default Child

After that, if we press the increment or decrement button, our console will look like this:

image.png

Just the initial rendering of the child component and no unnecessary re-renders when the prop hasn't changed. Piece of cake. ;)

Memo, useCallback and useMemo

We've covered class components, but in functional components we can't extend the React.PureComponent class. Instead, React offers one HOC and two hooks to deal with memoization.

Memo

If we transform our previous example to functional components we would get the following:

import { useState } from 'react'
import Child from "./child"

export default function Counter() {

    const [count, setCount] = useState(0)

    const handleIncrement = () => setCount(count+1)
    const handleDecrement = () => setCount(count-1)

    return (
        <div className="App">
            {console.log('parent')}
            <button onClick={() => handleIncrement()}>Increment</button>
            <button onClick={() => handleDecrement()}>Decrement</button>

            <h2>{count}</h2>

            <Child name={"Skinny Jack"} />
        </div>                    
    )
}
import React from 'react'

export default function Child({name}) {
console.log("Skinny Jack")
  return (
    <div>{name}</div>
  )
}

This would provoke the same problem than before, were the Child component re-rendered unnecessarily. To solve it, we can wrap our child component in the memo higher order component, like following:

import React from 'react'

export default React.memo(function Child({name}) {
console.log("Skinny Jack")
  return (
    <div>{name}</div>
  )
})

A higher order component or HOC, is similar to a higher order function in javascript (I have an article about that here). Higher order functions are functions that take other functions as arguments OR return other functions. React HOCs take a component as prop, and manipulate it to some end without actually changing the component itself. These can be thought like wrapper components.

In this case, memo that does a similar job to PureComponent, avoiding unnecessary re-renders of the components it wraps.

useCallback

An important thing to mention, is that memo doesn't work if the prop being passed to the component is a function. Let's refactor our example to see this:

import { useState } from 'react'
import Child from "./child"

export default function Counter() {

    const [count, setCount] = useState(0)

    const handleIncrement = () => setCount(count+1)
    const handleDecrement = () => setCount(count-1)

    return (
        <div className="App">
            {console.log('parent')}
            <button onClick={() => handleIncrement()}>Increment</button>
            <button onClick={() => handleDecrement()}>Decrement</button>

            <h2>{count}</h2>

            <Child name={console.log('Really Skinny Jack')} />
        </div>                    
    )
}
import React from 'react'

export default React.memo(function Child({name}) {
console.log("Skinny Jack")
  return (
    <>
        {name()}
        <div>Really Skinny Jack</div>
    </>
  )
})

Now our prop is a function that logs always the same string, and our console will look again like this:

image.png

This is because in reality a new function is being created on every parent component re-render. So if a new function is being created, that means we have a new prop and that means our child component should re-render as well.

To deal with this problem, react provides the useCallback hook. We can implement it in the following way:

import { useState, useCallback } from 'react'
import Child from "./child"

export default function Counter() {

    const [count, setCount] = useState(0)

    const handleIncrement = () => setCount(count+1)
    const handleDecrement = () => setCount(count-1)

    return (
        <div className="App">
            {console.log('parent')}
            <button onClick={() => handleIncrement()}>Increment</button>
            <button onClick={() => handleDecrement()}>Decrement</button>

            <h2>{count}</h2>

             <Child name={ useCallback(() => {console.log('Really Skinny Jack')}, [])  } />
        </div>                    
    )
}

And that solves the problem of unnecessary child re-rendering.

What useCallback does is to hold on to the value of the function despite the parent component re-rendering, so the child prop will remain the same as long the function value remains the same as well.

To use it, we just need to wrap the useCallback hook around the function we're declaring. In the array present in the hook, we can declare variables that would trigger the change of the function value when the variable changes too (exactly the same way useEffect works).

const testingTheTest = useCallback(() => { 
    console.log("Tested");
  }, [a, b, c]);

useMemo

useMemo is a hook very similar to useCallback, but instead caching a function, useMemo will cache the return value of a function.

In this example, useMemo will cache the number 2.

const num = 1
const answer = useMemo(() => num + 1, [num])

While useCallback will cache () => num + 1.

const num = 1
const answer = useMemo(() => num + 1, [num])

useMemo can be used in a very similar way to the memo HOC. The difference is useMemo is a hook with an array of dependences, and useMemo is a HOC that accepts as parameter an optional function that uses props to conditionally update the component. Moreover, useMemo caches a value returned between renders, while memo caches a whole react component between renders.

When to memoize

Memoization is a good tool to have in our belts, but it's not something to be used everywhere. These tool are thought for implementing when dealing with functions or tasks that require heavy computation. We have to be aware that in the background all of these three solutions add overhead to our code too. So if the re render is caused by a tasks that's not computationally heavy, it may be better to solve it in other way or leave it alone.

It should be pretty rare to have the need to implement these solutions unless we're seeing an important performance issue due to unnecessary re-rendering (shouldn't happen if we can keep our components lightweight) and if we can't solve the problem in a cleaner way.

If my article was helpful, consider inviting me a coffee = )

Invitame un café en cafecito.app

Buy me a coffee

You can also follow me on Twitter and Linkedin