DEV Community

Nicholas Fragiskatos
Nicholas Fragiskatos

Posted on • Originally published at blog.nicholasfragiskatos.dev on

How to Use Memoization in React for Better Performance

Memoization, not memorization, is a technique commonly used in functional programming to improve performance of an application. To put it simply, memoization is just caching the result of a function, then whenever that function is invoked again, as long the inputs are the same, the cached result can be returned instead of re-running the function. It is important to note that the memoized function would need to be a pure function. That is:

  1. The returned value is always the same for an identical set of inputs.

  2. The function does note mutate state outside of its scope, i.e., there are no side-effects.

React, which heavily encourages the use of a functional style of programming, naturally provides its own mechanisms for developers to optimize their application through memoization. With React.memo(...), useCallback(...), and useMemo(...), developers can have finer control over their application to prevent unnecessary re-rendering of components or heavy recalculations of data.

React.memo(...)

The purpose of React.memo(...) is to prevent unwanted re-rendering of a child component if its props have not changed when the parent component is re-rendered.

The Problem

Consider this <Parent/> component. It contains a button that, when clicked, increments a total state variable and it displays the current value of total. Lastly, it contains a <Child/> component that does not take any props.

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

const Parent = () => {

    const [total, setTotal] = useState(0)

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <div className="mt-3" />
            <Child />
        </div>
    )
}

export default Parent
Enter fullscreen mode Exit fullscreen mode

The <Child/> component only displays how many times it has been rendered. There's a useRef(...) paired with a useEffect(...) that keeps track of the number of renders for the component, and it displays that count.

import { useEffect, useRef } from "react"

const Child = () => {
    console.log("Child Rendered")
    const renderCount = useRef(1)

    useEffect(() => {
        renderCount.current = renderCount.current + 1
    })

    return (
        <div>
            <div className="text-3xl">Child Component</div>
            <div>Render Count: {renderCount.current}</div>
        </div>
    )
}
export default Child
Enter fullscreen mode Exit fullscreen mode

All together these two components create this UI

Picture of UI

Let's see what happens when we press the button to increment the total state variable of the parent component.

Gif showing child component re-rendering when parent re-renders

The child component re-renders with the parent component. This seems odd, especially since the child component's props are not changing. In fact, it does not even have any props.

The Solution

Fortunately, the solution is simple; wrap the child component in React.memo(Component, proprsComparatorFunction). Now when the parent component re-renders the child component does not.

import { useEffect, useRef, memo } from "react" // import

const Child = () => {
    ...
}

export default memo(Child) // wrapping component
Enter fullscreen mode Exit fullscreen mode

Gif showing how React.memo prevents the child component re-rendering when the parent component re-renders

The Comparator Function

React.memo(...) has a second argument for a comparator function, which wasn't utilized in the example. By default, if no function is specified, then React will do a shallow comparison of the previous and new prop values. A shallow comparison is fine if using simple data types like comparing numbers or booleans, but if the prop is something more complex like an object, then a custom implementation might be necessary.

useCallback(...)

The purpose of useCallback(...) is to memoize a function that's defined in the component. If a function is defined in a component, then it will be recreated during each re-render. useCallback(...) can prevent his. The benefit to stopping the recreation of the function is exemplified best when functions are being passed down as props to child components.

The Problem

Consider the same <Parent/> and <Child/> components from before with the same React.memo(...) solution used to prevent re-renders. Except this time <Child/> defines a prop for a callback function, and <Parent/> defines a function called myFunction and passes it down to <Child/>. Also, aside from defining the callback prop, <Child/> doesn't even do anything with it.

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

const Parent = () => {

    const [total, setTotal] = useState(0)

    const myFunction = () => {
        console.log(`NON-Callback function called`)
    }

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent
Enter fullscreen mode Exit fullscreen mode
import { memo, useEffect, useRef } from "react"

const Child = ({ callback }: { callback: () => void }) => {

    console.log("Child Rendered")
    const renderCount = useRef(1)

    useEffect(() => {
        renderCount.current = renderCount.current + 1
    })

    return (
        <div>
            <div className="text-3xl">Child Component</div>
            <div>Render Count: {renderCount.current}</div>
        </div>
    )
}

export default memo(Child)
Enter fullscreen mode Exit fullscreen mode

Again, let's see what happens when we change state in the parent component.

Gif showing the child component re-rendering when the parent component re-renders

All of React.memo(...)'s hard work is undone. Although, React.memo(...) is doing exactly what it's supposed to be doing, but there are two problems:

  1. Each re-render of <Parent/>, myFunction(...) gets recreated completely, and occupies a different space in memory, even though nothing has changed about it.

  2. React.memo(...) is still doing a shallow comparison, but since the old function prop and the new function prop are not referencing the same spot in memory, it thinks that the prop is changed, so <Child/> needs to be re-rendered. Remember functions in JavaScript are objects and objects are compared by reference.

The Solution

Again, the solution is simple; stop recreating the function and just use the old function by using the useCallback(fn, dependencyList) hook. The first argument is the function to define whatever work needs to be done, and the second argument, similar to useEffect(...), is for a list of state variables that useCallback will check. As long as none of the state dependencies in the list change, then on every render the same function will be returned instead of creating a new one. Now since the function is the exact same, even from a referential perspective, React.memo(...)'s shallow comparison will not trigger a re-render of <Child/>

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

const Parent = () => {

    const [total, setTotal] = useState(0)

    const myFunction = useCallback(() => {
        console.log(`Callback function called`)
    }, [])

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent
Enter fullscreen mode Exit fullscreen mode

Gif showing child component no longer re-rendering when parent component re-renders

useMemo(...)

Lastly, useMemo(...) is used to cache the result of a function. When rendering involves calling some function that produces some value, and the function is doing an expensive calculation but no inputs to the function are changing, then there's no point in the recalculation during each re-render.

The Problem

Continuing from the last code snippet, let's add a new state variable called myName, a function called uppercaseName, and then in the UI let's add a text field and a <p> tag to display the uppercase name.

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

const Parent = () => {

    const [total, setTotal] = useState(0)
    const [myName, setMyText] = useState("michael scott")

    const myFunction = useCallback(() => {
        console.log(`Callback function called`)
    }, [])

    const uppercaseName = () => {
        console.log("Processing Name...")
        return myName.toUpperCase()
    }

    return (
        <div className="flex flex-col gap-1 w-1/5">
            <h1 className="text-3xl">Parent Component</h1>
            <div>Total: {total}</div>
            <input className="border-2 p-1" type="text" value={myName} onChange={(e) => setMyText(e.target.value)} />
            <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button>
            <p>{`Hello, this is ${uppercaseName()}`}</p>
            <div className="mt-3" />
            <Child callback={myFunction} />
        </div>
    )
}

export default Parent
Enter fullscreen mode Exit fullscreen mode

Picture showing UI with text field and paragraph added

Typing something in the text field will display an uppercase version in the paragraph below the button.

Gif showing what happens when typing in the text field

However, if we open the console and start using the button to increment the count, we can see the function is being ran each time during the re-render even though the name has not changed.

Gif showing recalculations on every render

The Solution

We can use useMemo(...) to wrap the uppercaseName function in a similar way we used useCallback(...). Except this time the result of useMemo(...) is a value, and not a function. Like useCallback(...), it takes a dependency list, and as long as the state in the dependency list does not change, the result does not have to be recalculated.

import { useCallback, useState } from "react"import Child from "./Child"const Parent = () => { const [total, setTotal] = useState(0) const [myName, setMyText] = useState("michael scott") const myFunction = useCallback(() => { console.log(`Callback function called`) }, []) const uppercaseName = useMemo(() => { console.log("Processing Name...") return myName.toUpperCase() }, [myName]) return ( <div className="flex flex-col gap-1 w-1/5"> <h1 className="text-3xl">Parent Component</h1> <div>Total: {total}</div> <input className="border-2 p-1" type="text" value={myName} onChange={(e) => setMyText(e.target.value)} /> <button className="border-2 bg-cyan-600 p-1 border-black rounded-md text-white" onClick={() => setTotal(total + 1)}>Increment Parent Count</button> <p>{`Hello, this is ${uppercaseName}`}</p> <div className="mt-3" /> <Child callback={myFunction} /> </div> )}export default Parent
Enter fullscreen mode Exit fullscreen mode

Gif showing the calculation no longer being done needlessly

Over Optimization

After learning about these optimization techniques it may be tempting to start using them everywhere. However that might be overkill for most scenarios. In fact, the React documentation advises to only use these techniques for specific scenarios. However, it is ultimately up to you to decide what is best for your application and when to use what and where to use it.

Should you add React.memo everywhere?

Should you add useCallback everywhere?

Should you add useMemo everywhere?

Conclusion

React provides its own mechanisms to developers to optimize their application through memoization.

Through the use of React.memo(...) we can limit the number of re-renders of a child component when the parent component re-renders. React.memo(...) will memoize the child component and only re-render if its props have changed.

Utilizing useCallback(...) we can prevent the unnecessary recreation of functions each time a component gets re-rendered. Furthermore, we saw how this can be used in conjunction with React.memo(...) to limit child component re-renders when passing functions down as props.

Lastly, useMemo(...) can be used to avoid duplicate recalculations and data processing, especially if it is an expensive operation. As long as dependent state for the calculation does not change, there is no reason to perform the operation.


Thank you for taking the time to read my article. I hope it was helpful.

If you noticed anything in the article that is incorrect or isn't clear, please let me know. I always appreciate the feedback.

Top comments (0)