Re-rendering React components unnecessarily can slow down your app and make the UI feel unresponsive. This makes the user experience worse and causes a higher Interaction to Next Paint metric.
This article explains how to update components only when necessary, and how to avoid common causes of unintentional re-renders.
Use React.memo or React.PureComponent
When a component re-renders, React will also re-render child components by default.
Here's a simple app with two Counter
components and a button that increments one of them.
function App() {
const [counterA, setCounterA] = React.useState(0);
const [counterB, setCounterB] = React.useState(0);
return (
<div>
<Counter name="A" value={counterA} />
<Counter name="B" value={counterB} />
<button
onClick={() => {
console.log("Click button");
setCounterA(counterA + 1);
}}
>
Increment counter A
</button>
</div>
);
}
function Counter({ name, value }) {
console.log(`Rendering counter ${name}`);
return (
<div>
{name}: {value}
</div>
);
}
Right now, both Counter
components render when the button is clicked, even though only counter A has changed.
Click button
Rendering counter A
Rendering counter B
The React.memo
higher-order component (HOC) can ensure a component is only re-rendered when its props change.
const Counter = React.memo(function Counter({ name, value }) {
console.log(`Rendering counter ${name}`);
return (
<div>
{name}: {value}
</div>
);
});
Now only counter A is re-rendered, because it's value
prop changed from 0
to 1
.
Click button
Rendering counter A
For class-based components
If you're using class-based components instead of function components, change extends React.Component
to extends React.PureComponent
to get the same effect.
Make sure property values don't change
Preventing the render in our example was pretty easy. But in practice this is more difficult, as it's easy for unintentional prop changes to sneak in.
Let's include the Increment button in the Counter
component.
const Counter = React.memo(function Counter({ name, value, onClickIncrement }) {
console.log(`Rendering counter ${name}`);
return (
<div>
{name}: {value} <button onClick={onClickIncrement}>Increment</button>
</div>
);
});
The App
component now passes in an onClickIncrement
prop to each Counter
.
<Counter
name="A"
value={counterA}
onClickIncrement={() => setCounterA(counterA + 1)}
/>
If you increment counter A, both counters are re-rendered.
Rendering counter A
Rendering counter B
Why? Because the value of the onClickIncrement
prop changes every time the app re-renders. Each function is a distinct JavaScript object, so React sees the prop change and makes sure to update the Counter
.
This makes sense, because the onClickIncrement
function depends on the counterA
value from its parent scope. If the same function was passed into the Counter
every time, then the increment would stop working as the initial counter value would never update. The counter value would be set to 0 + 1 = 1
every time.
The problem is that the onClickIncrement
function changes every time, even if the counter value it references hasn't changed.
We can use the useCallback
hook to fix this. useCallback
memoizes the function that's passed in, so that a new function is only returned when one of the hook dependencies changes.
In this case the dependency is the counterA
state. When this changes, the onClickIncrement
function has to update, so that we don't use outdated state later on.
<Counter
name="A"
value={counterA}
onClickIncrement={React.useCallback(
() => setCounterA(counterA + 1),
[counterA],
)}
/>
If we increment counter A now, only counter A re-renders.
Rendering counter A
For class-based components
If you're using class-based components, add methods to the class and use the bind
function in the constructor to ensure it has access to the component instance.
constructor(props) {
super(props)
this.onClickIncrementA = this.onClickIncrementA.bind(this)
}
(You can't call bind
in the render function, as it returns a new function object and would cause a re-render.)
Run A Free Page Speed Test
Test Your Website:
- No Login Required
- Automated Recommendations
- Google SEO Assessment
Passing objects as props
Unintentional re-renders not only happen with functions, but also with object literals.
function App() {
return <Heading style={{ color: "blue" }}>Hello world</Heading>;
}
Every time the App
component renders a new style object is created, leading the memoized Heading
component to update.
Luckily, in this case the style object is always the same, so we can just create it once outside the App
component and then re-use it for every render.
const headingStyle = { color: "blue" };
function App() {
return <Heading style={headingStyle}>Hello world</Heading>;
}
But what if the style is calculated dynamically? In that case you can use the useMemo
hook to limit when the object is updated.
function App({ count }) {
const headingStyle = React.useMemo(
() => ({
color: count < 10 ? "blue" : "red",
}),
[count < 10],
);
return <Heading style={headingStyle}>Hello world</Heading>;
}
Note that the hook dependency is not the plain count
, but the count < 10
condition. That way, if the count changes, the heading is only re-rendered if the color would change as well.
children props
We get the same problems with object identity and unintentional re-renders if the children we pass in are more than just a simple string.
<Heading>
<strong>Hello world</strong>
</Heading>
However, the same solutions apply. If the children are static, move them out of the function. If they depend on state, use useMemo
.
function App({}) {
const content = React.useMemo(
() => <strong>Hello world ({count}</strong>,
[count],
);
return (
<>
<Heading>{content}</Heading>
</>
);
}
Using keys to avoid re-renders
Key props allow React to identify elements across renders. They're most commonly used when rendering a list of items.
If each list element has a consistent key, React can avoid re-rendering components even when list items are added or removed.
function App() {
console.log("Render App");
const [items, setItems] = React.useState([{ name: "A" }, { name: "B" }]);
return (
<div>
{items.map((item) => (
<ListItem item={item} />
))}
<button onClick={() => setItems(items.slice().reverse())}>Reverse</button>
</div>
);
}
const ListItem = React.memo(function ListItem({ item }) {
console.log(`Render ${item.name}`);
return <div>{item.name}</div>;
});
Without the key on <ListItem>
we're getting a Warning: Each child in a list should have a unique "key" prop
message.
This is the log output when clicking on the Reverse button.
=> Reverse
Render app
Render B
Render A
Instead of moving the elements around, React instead updates both of them and passes in the new item
prop.
Adding a unique key to each list item fixes the issue.
<ListItem item={item} key={item.name} />
React can now correctly recognize that the items haven't changed, and just moves the existing elements around.
What's a good key?
Keys should be unique, and no two elements in a list should have the same key. The item.name
key we used above isn't ideal because of this, as multiple list elements might have the same name. Where possible, assign a unique ID to each list item – often you'll get this from the backend database.
Keys should also be stable. If you use Math.random()
then the key will change every time, causing the component to re-mount and re-render.
For static lists, where no items are added or removed, using the array index is also fine.
Keys on fragments
You can't add keys to fragments using the short syntax (<>
), but it works if you use the full name:
<React.Fragment key={item.name}></React.Fragment>
Monitor Page Speed & Core Web Vitals
DebugBear monitoring includes:
- In-depth Page Speed Reports
- Automated Recommendations
- Real User Analytics Data
Avoid changes in the DOM tree structure
Child components will be remounted if the surrounding DOM structure changes. For example, this app adds a container around the list. In a more realistic app you might put items in different groups based on a setting.
function App() {
console.log("Render App");
const [items, setItems] = React.useState([{ name: "A" }, { name: "B" }]);
const [showContainer, setShowContainer] = React.useState(false);
const els = items.map((item) => <ListItem item={item} key={item.name} />);
return (
<div>
{showContainer > 0 ? <div>{els}</div> : els}
<button onClick={() => setShowContainer(!showContainer)}>
Toggle container
</button>
</div>
);
}
const ListItem = React.memo(function ListItem({ item }) {
console.log(`Render ${item.name}`);
return <div>{item.name}</div>;
});
When the parent component is added all existing list items are unmounted and new component instances are created. React Developer Tools shows that this is the first render of the component.
Where possible, keep the DOM structure the same. For example, if you need to show dividers between groups within the list, insert the dividers between the list elements, instead of adding a wrapper div to each group.
Monitor the performance of your React app
DebugBear can track the load time and CPU activity of your website over time. Just enter your URL to get started.