Component re-rendering is expensive and can affect performance. This article explains simple methods to avoid unnecessary component re-renders.
React components re-render when their state or props change. When a react component re-renders, all its child components also re-render. That is a chain reaction that affects components entire child components tree. Most of the time, these re-renders are unnecessary and negatively affect the component performance.
In the below-listed example, we update the parent components state by setting showParagraph to true. As a result, React updates the <Form/> component, and the entire child component tree that belongs to Form. Note that the Form component neither accepts any properties nor updates any state variables. Therefore, its re-render is not necessary.
import { useCallback, useState } from 'react';
import {Form} from './components/Form';
import {Button} from './components/Button';
export const App = () => {
const [showParagraph, setShowParagraph] = useState(false)
const toggleParagraph = () => {
setShowParagraph((prev) => !prev)
}
console.log("App Re-rendered")
return (
<>
{showParagraph && <p>Information</p>}
<Form/>
<Button onClick={toggleParagraph}/>
</>
);
}
export default App;
App.tsx contains two child components, Button and Form. This button toggles a paragraph based on the showParagraph variable value. When we update showParagraph by clicking the button, Rect updates the entire component tree under App.tsx. Note the console logs in every component to detect re-renders.
Note that <Form/> has no dependency on App.js. But the Form component re-renders whenever the showParagraph changes. Hence, the Input component also re-renders.
import {Input} from "./Input"
export const Form = () => {
console.log("Form Re-rendered")
return (
<>
<Input name="name"/>
<Input name="age"/>
</>
)
}
interface InputProps {
name: string
}
export const Input = (props: InputProps) => {
console.log(`Input ${props.name} Re-rendered`)
return(
<div><input name={props.name} /></div>
)
}
The button component toggles the showParagraph. The Button component also re-renders when the showParagraph changes, although it is unnecessary.
interface ButtonProps {
onClick: () => void
}
export const Button = (props: ButtonProps) =>{
console.log("Button Re-rendered")
return(
<div>
<button type="button" onClick={() => props.onClick()}>Toggle</button>
</div>
)
}
We can avoid these re-renders by following two simple steps.
We can avoid re-renders by simply exporting the component using React.memo. React.memo tells React to compare the Component props with the previous props and only re-render if they are different.
import React from "react"
import Input from "./Input"
const Form = () => {
console.log("Form Re-rendered")
return (
<>
<Input name="name"/>
<Input name="age"/>
</>
)
}
export default React.memo(Form)
import React from "react"
interface ButtonProps {
onClick: () => void
}
const Button = (props: ButtonProps) =>{
console.log("Button Re-rendered")
return(
<div>
<button type="button" onClick={() => props.onClick()}>Toggle</button>
</div>
)
}
export default React.memo(Button)
The React.memo will take care of any prop changes and handle re-renders efficiently. But you will notice that doesn't solve the issue for the Button component. That's because the Button receives a function reference as a prop. This referencing function is re-created every time the App.tsx is re-rendered, so as the function reference. Therefore, the current prop and previous prop are never equal. props.onClick === prev.props.onClick is never true. Because they are different references.
The useCallback hook memoizes the function it wraps and returns the same reference between re-renders. Meaning, that React stores the toggleParagraph function in the memory, and returns the same reference every time the App.tsx is re-rendered.
import { useCallback, useState } from 'react';
import Form from './components/Form';
import Button from './components/Button';
export const App = () => {
const [showParagraph, setShowParagraph] = useState(false)
const toggleParagraph = useCallback(() => {
setShowParagraph((prev) => !prev)
},[])
console.log("App Re-rendered")
return (
<>
{showParagraph && <p>Information</p>}
<Form/>
<Button onClick={toggleParagraph}/>
</>
);
}
export default App;
useCallback accepts two arguments.
If any dependency argument is changed, React re-creates the function. An empty array means React will only create the function once when the App.tsx is initially created.
useCallback and React.memo uses resources.