How to avoid Unnecessary rerenders in React

If you're building anything that's computation-heavy/animation-heavy with a big number of components, re-rendering can slow down your app's speed. Let's examine how we can avoid re-rendering using React.memo and a few other techniques

Published By Shyam Lohar on 09 Aug, 2020


There are several methods we can use as a baseline to prevent unneeded renderings.

State in Parent Component

We frequently maintain data in a parent component that is only needed by a few child components. Result? When the state of the parent component is updated, all of our child components re-render.

Solution?

Create a new component to hold the state and pass it down to the child components.

Let me show you what I meant by that

const Parent = () => {
  const [state, dispath] = useReducer(reducer, initialState)

  return (
    <>
      <Navbar />
      <ChildA state={state} dispath={dispath} />
      <ChildB state={state} dispath={dispath} />
      <ChildC state={state} dispath={dispath} />
      <ChildD />
      <ChildE />
      <Footer />
    </>
  )
}

As we can see components on lines number 6 and 10 to 12 don't need a state at all still whenever the state of Parent component is updated all the components will rerender.

How do we fix it?

There are multiple ways to fix it let's look at all possible solutions one by one.

  1. Create one new component which holds the ChildA, ChildB and ChildC components state and passes it to those three components that way we can avoid rerenders of other components.
const FeatureX = () => {
  const [state, dispath] = useReducer(reducer, initialState)
  return (
    <>
      <ChildA state={state} dispath={dispath} />
      <ChildB state={state} dispath={dispath} />
      <ChildC state={state} dispath={dispath} />
    </>
  )
}
const Parent = () => {
  return (
    <>
      <Navbar />
      <FeatureX />
      <ChildD />
      <ChildE />
      <Footer />
    </>
  )
}

Now Whenever the state is updated inside the FeatureX component only FeatureX component (including child component present in FeatureX) will rerender. Simple enough?

Try to keep your components dumb but if some components need state try to isolate that state to some component that holds all components that need state (just like we did with FeatureX component) so that other components don't rerender.

  1. Using React.memo

React.memo is a higher-order component provided by React. It achieves certain efficiencies by eliminating component redraws if the props given to those components remain unchanged.

Lets take a look at code snippet again.

const Parent = () => {
  const [state, dispath] = useReducer(reducer, initialState)

  return (
    <>
      <Navbar />
      <ChildA state={state} dispath={dispath} />
      <ChildB state={state} dispath={dispath} />
      <ChildC state={state} dispath={dispath} />
      <ChildD />
      <ChildE />
      <Footer />
    </>
  )
}

Let's say you can't move the state from a parent component for X reason. Instead, you can wrap components on highlighted lines in React.memo HOC.

const Navbar = React.memo(() => {
  return <>{/* Navbar markup */}</>
})

since we are not passing any props to the Navbar component every time Parent components react will check if props passed to the Navbar component is changed or not.

if props did not change react won't rerenderNavbarcomponent. since we are not passing any props to the Navbar component react will conclude props did not change and react won't rerender the Navbar component. We can do the same with all other components on highlighted lines and those components will stop getting rerendered.

Of course, Memoization has overhead but react does shallow prop equality check and it is not that expensive operation. The cost of memorization is often less than the cost of re-renders. If you are more interested in details you can read it in this amazing blog post

  1. Using useCallback

Let's take a look at code snippet again. We have created very simple Counter component for sake of simplicity.

import { useState } from "react"

const Button = ({ onClick }) => {
  console.log("I am Updated")
  return <button onClick={onClick}>Increment</button>
}

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      {count}
      <Button onClick={() => setCount((c) => c + 1)} />
    </div>
  )
}

We have Button component which is passed a updateCount function and on click of button setCount function is called with incremented count and count is incremented.

Its very clear that our Button component does not need to know about what is the state of the counter. only thing our Button component needs to know is which function to call when it is clicked.

But whenever count is updated our Button component gets rerendered. This entire blog post is about avoiding rerenders so lets just do that lets avoid that rerender. How? Lets apply whatever we read above.

  1. Isolate state? State is already isolated.
  2. Use React.memo? Yes.
import React, { useState } from "react"

const Button = React.memo(({ onClick }) => {
  console.log("I am Updated")
  return <button onClick={onClick}>Increment</button>
})

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

  return (
    <div className="App">
      {count}
      <Button onClick={() => setCount((c) => c + 1)} />
    </div>
  )
}

Result? We will still see that our Button component gets rerendered on every click of the button even tho we have wrapped it in React.memo HOC.

Why?

React recreates the callback function passed to the Button component every time the Counter component is re-rendered.

When React.memo checks whether or not the props supplied to the Button component have changed, it now concludes that they have.

Even tho functionality of our function is same but reference to the function is different and shallow check of React.memo fails.

Remember {} === {} results in false even tho both are empty object but refernce is not same. you can read more about it in this blog post

How do we fix it?

useCallback Hook to the rescue!

Lets look at what useCallback hook does from react docs itself

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders

Lets exactly do that refactor our code to use useCallback hook and avoid rerender 😉

import React, { useState } from "react"

const Button = React.memo(({ onClick }) => {
  console.log("I am Updated")
  return <button onClick={onClick}>Increment</button>
})

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

  const updateCount = useCallback(() => setCount((c) => c + 1), [])

  return (
    <div className="App">
      {count}
      <Button onClick={updateCount} />
    </div>
  )
}

Now whenever we click on the button our Button component will not get rerendered.

How?

We just memoized our updateCount function and we are passing it as props to Button component.

useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed and since we are passing empty array as dependency we will get same callback every time and our React.memo's shallow check will pass and our Button component will not get rerendered.

TLDR

  1. Isolate state
  2. Use React.memo to memoize components if props are not expected to change often
  3. Use useCallback to memoize callbacks