DEV Community

Cover image for Understanding ReactJS re-renders and memoization
m0nm
m0nm

Posted on

Understanding ReactJS re-renders and memoization

Performance is a core factor in any successful web apps, Users won't bother to wait over 2 seconds for websites to finish loading, and because of that you -the web developer- should make sure that your websites doesn't exceed that.

In this tutorial, We're going to take a look at ReactJS rendering cycle, how does it work, what trigger a render, how do we optimize it using methods such as: useMemo and useCallback, and when we should (or shouldn't) use them.

Table of Contents

What is rendering

Simply means that ReactJS draws the shape (UI) of a component based on its states and props. It does that by converting the component code into html elements and inject it into the DOM.

The ReactJS rendering cycle

When it comes to converting a component code into html elements ReactJS split this process into two steps: the render step and the commit step. however this process behavior is slightly different depending on if the components are initialized or re-rendered

When initializing the component

  • the render step
    Here React will traverse the DOM from the root element to all it's descendants, while traversing React will convert JSX into a React Element using the createElement() method

  • the commit step
    here all React Elements are applied to the DOM

When re-rendering a component

React will traverse the DOM in search of elements that are flagged for update. once finished it will proceed to the render step

  • the render step
    Similarly to initializing the component, React will convert JSX into a React Element for each element flagged for update using the createElement() method. Then they'll be compared to the their last render creating a list of all changes that needs to be made in the DOM

  • the commit step
    Finally all elements that are different from themselves in the last render will be applied to the DOM, Otherwise React will discard the operation

sometimes the rendering step can be slow (ex: component is too big), which causes performance hits, therefore if we need to eliminate those performance hits we should remove unnecessary re-renders. Next we'll discuss what triggers a re-render.

What triggers a re-render?

There are 3 main factors that cause a re-render

The component state has changed

You've probably knows this, Changing the state of a component (either using useState or useReducer) will cause a re-render so that the website UI match the component, Here is a basic example

import {useState, useEffect} from "react"

function APP() {
    const [count, setCount] = useState(0)

    useEffect(() => console.log('new render'), [])

    return <button onClick={() => setState(count + 1)}>click me</button>
}
Enter fullscreen mode Exit fullscreen mode

each time you click on the button will fire a console log implying a re-render.

Speaking of state hooks, You may find this interesting:

The component parent is re-rendered

If the parent component triggered a re-render (either by these 4 factors) the component will also re-render, Example


function Parent() {
    const [count, setCount] = useState(0)

    return (
        <div>
            <button onClick={() => setState(count + 1)}>click me</button>

            <Child />
        </div>
    )
}

function Child() {
    useEffect(() => {
        console.log("I'm being re-rendered because of my parent 😠")
    }, [])

    return <h1>I'm a child component</h1>
}
Enter fullscreen mode Exit fullscreen mode

each time you click on the button it will fire a console log with the message I'm being re-rendered because of my parent 😠, implying a re-render of the child component.

Context value has changed

if the component is consuming context provider value then any change in the value will re-render the component

import {useState,createContext, useContext} from "react"

const CountContext = createContext(null)

function App() {
    const [count, setCount] = useState(1);

    const onClick = () => {
        setCount(prev => prev + 1);
    };

    <CountContext.Provider value={count}>
        <Component />
    </CountContext.Provider>
}

function Component() {
    const value = useContext(CountContext)
    console.log("re-render")
    return <h1>count is: {value}</h1>
}

Enter fullscreen mode Exit fullscreen mode

What about props change?

Props are passed from parent to child, In most cases props are a state in the parent component, When props changes that means the parent's state has changed which causes the parent to re-render, which means the child will re-render as well as stated above

Memoization techniques

Now we're going to explore memo, useMemo and useCallback , how they work and how to use them

memo

when we have a Parent component and Child component with it's props. We know that when the parent component re-renders then all of it's descendants will re-render as well, Even if their props hasn't changed. The reason is that React compares the old props with the new props which do not reference the same object, therefore React will re-render the child component.

If it happens to be a problem to you, You can use React higher order component memo to memoize the Child component:

// ./Child.jsx
import {memo} from "react"

function Child({name}) {
    console.log("not gonna re-render when my parent do")
    return <h1>My memoized name is: {name}</h1>
}

export default memo(Child)

// ./Parent.jsx
function Parent() {
    const [count, setCount] = useState(1);
    const name = "john"

    return(
        <button onClick={() => setState(count + 1)}>click me</button>
        <Child name={name} />
    )
}
Enter fullscreen mode Exit fullscreen mode

Composition

Another trick you could use without relying on memo is composition, more about it by Kent Dodds


function Child({name}) {
    console.log("not gonna re-render when my parent do")
    return <h1>My memoized name is: {name}</h1>
}

// ./Parent.jsx
function Parent({children}) {
    const [count, setCount] = useState(1);

    return(
        <button onClick={() => setState(count + 1)}>click me</button>
        {children}
    )
}

function App() {
    const name = "john"

    return (
        <Parent>
            <Child name={name} />
        </Parent>
    )
}
Enter fullscreen mode Exit fullscreen mode

Using component's props

You can achieve the same by passing the Child as a prop to Parent

function Child({name}) {
    console.log("not gonna re-render when my parent do")
    return <h1>My memoized name is: {name}</h1>
}

// ./Parent.jsx
function Parent({child}) {
    const [count, setCount] = useState(1);

    return(
        <button onClick={() => setState(count + 1)}>click me</button>
        {child}
    )
}

function App() {
    const name = "john"

    return (
        <Parent child={<Child name={name} />} />
    )
}
Enter fullscreen mode Exit fullscreen mode

useMemo

When React re-renders every function within the component will be re-created again, this could cause performance issues if the function is expensive. In order to fix that useMemo allows you to memoize expensive functions so that you can avoid calling them on every render.

You can see below we have expensiveFunction that is slow, by passing it to useMemo we make sure that it's not called again unless we change the bool state, so that when we click on "increment count" button there won't be any lag:

import {useState, useMemo} from "react"
function App() {
    const [count, setCount] = useState(1);
    const [bool, setBool] = useState(false);

    const expensiveFunction = useMemo(() => {
        for(let i = 0; i < 100000000; i++) {}
        // do something
        return bool
    } , [bool])

    return (
        // increment count
        <button onClick={() => setCount(count + 1)}>increment count</button>

        // re render to use expensiveFunction
        <button onClick={() => setBool(!bool)}>re-render</button>
    )
}
Enter fullscreen mode Exit fullscreen mode

useCallback

The useCallback and useMemo Hooks are similar. The main difference is that useMemo returns a memoized value and useCallback returns a memoized function.

import {useState, useCallback} from "react"
function App() {
    const [count, setCount] = useState(1);
    const [bool, setBool] = useState(false);

    const expensiveFunction = useCallback(() => {
        for(let i = 0; i < 100000000; i++) {}
        // do something
    } , [bool])

    return (
        // increment count
        <button onClick={() => setCount(count + 1)}>increment count</button>

        // re render to use expensiveFunction
        <button onClick={() => setBool(!bool)}>re-render</button>
    )
}
Enter fullscreen mode Exit fullscreen mode

You're trading time with memory!

Now this doesn't mean to use these memoizing hooks in every piece of code of your app, Know that what you're doing is trading time with memory. Too much memory usage is just going to make it worse.

So when should you use them?

Only when you notice that your app is slow when re-rendering, Otherwise don't touch the code: "If it's not broken don't fix it"

Further Reading

If you want to go in depth about this topic, I recommend you read Mark Erikson post:
A (Mostly) Complete Guide to React Rendering Behavior

Conclusion

We've reached the end of this post, I hope it was helpful to you, If it was then please consider liking this post. If you have any questions just hit me up in the comment section, I'm not an expert in this subject but I'll try my best.

Top comments (0)