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.
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.
There are multiple ways to fix it let's look at all possible solutions one by one.
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.
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 rerenderNavbar
component. 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
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.
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 infalse
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
React.memo
to memoize components if props are not expected to change oftenuseCallback
to memoize callbacks