Featured image of post React Rendering Secrets

React Rendering Secrets

Learn the simple yet elusive secrets of efficiently rendering components in React

Setting the scene

Picture a parent component with some child components and we are updating the parent’s state. What will happen?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { useState } from 'react'
import ChildButton from './ChildButton'

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

  console.log('parent rendered')

  return (
    <>
      <div>The count is: {count}</div>
      <ChildButton onClick={() => setCount(c => c + 1)}>Increment</ChildButton>
    </>
  )
}

export default Parent
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { ReactNode } from "react"

interface ChildButtonProps {
  onClick?: VoidFunction
  children?: ReactNode
}

const ChildButton = ({ onClick, children }: ChildButtonProps) => {
  console.log('child rendered')

  return (
    <button onClick={onClick}>
      {children}
    </button>
  )
}

export default ChildButton

Note that I have added console.log to each component in order to track when they are being rendered.

So how does it renders?

Everything renders properly and after clicking on the Increment button 3 times, the result is as expected.

Result after 3 clicks

Now analyzing at the console output, we can see that the <Parent> was rendered 4 times. Most certainly when the value of count was 0, 1, 2, 3.

But why did the <ChildButton> had to be rendered 4 times also? Isn’t that a waste for resources since the <ChildButton> component didn’t change in any way?

While with minor components these additional rendering are harmless, it can affect the application’s performance when more complex components are being utilized.

Avoiding unnecessary re-rendering

We can use memo to tell React to skip re-rendering a component unless it’s props have changed. Here’s how it’s done:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { memo, ReactNode } from "react"

interface ChildButtonProps {
  onClick?: VoidFunction
  children?: ReactNode
}

const ChildButton = memo(({ onClick, children }: ChildButtonProps) => {
  console.log('child rendered')

  return (
    <button onClick={onClick}>
      {children}
    </button>
  )
})

ChildButton.displayName = "ChildButton"

export default ChildButton

Let’s now run our application again and do the same test.

Result after 3 clicks

It looks like nothing has changed! Why?

Change detection in React

Well it might look that nothing has changed but from a JavaScript point of view, everything has changed! In our <Parent> component, we are passing down props as follow:

1
<ChildButton onClick={() => setCount(c => c + 1)}>Increment</ChildButton>

React will do simple equality checks for change detection. Consider this:

1
2
3
4
5
6
7
1 === 1 // evaluates to true
"a" === "a" // evaluates to true
false === false // evaluates to true
null === null // evaluates to true

{} === {} // evaluates to false
(() => {}) === (() => {}) // evaluates to false

As you can see above, an empty function when compared using === to another similar empty function gives false.

So from React’s perspective, it looks like the props did change, so a re-render is triggered.

useCallback to the rescue

So how can we tell React that the onClick prop hasn’t changed?

We can use useCallback to do just that!

1
2
3
const handleClick = useCallback(() => {
  setCount(c => c + 1)
}, [])

The first parameter of useCallback is the function and the second parameter is the dependency list.

What is the dependency list? It is all variables (or const) used inside the function callback but declared in the component (including props).

Then why not include setCount? It is definitely declared in the component and used inside the callback function! Yes you can include setCount in the dependency list, but it will not affect the callback in any way since settors are stable across renders and never triggers change detection in React.

Result after using proper hooks

Again, the button is pressed thrice and voila!

Result after 3 clicks

You can now clearly see how unnecessary re-rendering has been avoided.

If you are more curious about React hooks, read about useMemo and useEffect from the official React docs

Built with Hugo
Theme Stack designed by Jimmy