In an extensive React application, excessive or unnecessary re-renders are common. This article addresses instances of excessive re-rendering in React applications and provides several ways to avoid them. It is written for readers with a basic understanding of React fundamentals and React Hooks.
Typically, React performs well out of the box using techniques to reduce the Document Object Model (DOM) operations required to update the user interface (UI) . These techniques lead to faster applications with fewer optimization processes, which improves user experience.
A React application can be built to full functionality under one component. However, a major downfall is that a single-component-based React application would re-render upon each state change. To update a stand-alone React application, users must pass the state through intermediate components.
This method slows application performance. Best practice suggests breaking down the components using the single responsibility principle. Using this principle, each component preferably holds one function. Such decomposition of the components into subcomponents dramatically improves application performance, code reusability and state management.
Adhering to the single responsibility principle solves many issues. However, it does not solve all issues. State management is another cause for concern in React applications. State management libraries such as Redux and MobX are introduced to React applications because managing state within individual components becomes cumbersome as applications grow in size and complexity. It's important to note that the potential for excessive re-rendering is especially present when using a state container.
With any React application, an app managed by a state container is rendered to update the UI when the user interacts with the app. Rendering is essentially the way by which React knows whether it needs to make changes to the DOM. The initial render occurs in two main phases: the render phase and the commit phase.
React will begin from the root of the component tree in the render phase and continue the process downwards to the child components. As it goes through each of the components, React invokes the createElement method, converts the component's JSX into React elements, then stores that render. Once this conversion is complete, the React elements are handed over to the commit phase, where they are applied to the DOM using the ReactDOM package.
A second or subsequent render to update the state is commonly referred to as a re-render. A re-render can be caused under any of the following three circumstances:
- When a prop in a component gets updated
- When a state in a component gets updated
- When a parent component’s render method gets called
Unnecessary re-renders slow down app performance, draining users’ batteries. Fortunately, these inefficiencies can be prevented with the right techniques. Read on to learn more about re-rendering and how to avoid excessive re-rendering.
When Do Re-Renders Happen?
Re-renders occur when a component's state or prop changes. When neither changes, no re-render occurs. Just like the initial render, a re-render follows the render and commit phase process. However, in the case of a re-render, React finds the components flagged for an update.
For all flagged components, the components’ JSX will be converted into React elements using the createElement method, and the result will be stored. React then undergoes reconciliation which principally involves diffing the old and new tree of the React elements over the virtual DOM. As stated in the React documentation:
"When you use React, at a single point in time, you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree."
Once the reconciliation process is complete, the result is handed over to the commit phase, then applied to the DOM. To further understand what happens under the hood with JSX when a render happens, check out this detailed blog post.
It is possible to accidentally create new props objects and force components to re-render unnecessarily. As we continue to delve deeper into how this is possible, let's first go through this illustration of what could cause a wasted render:
Say a user clicks a button that triggers an update in C4 above. The update then comes down from the root, traverses through P3 to get to C4 to make the necessary change. Under normal React circumstances, when the root renders, all its child components would automatically render. In this case, P1 and P2 would also automatically re-render alongside P3, same as their children, resulting in wasted renders as they do not affect any changes. You can now see an example of what slows your app and leads to battery drainage.
A good example of passing of props can be well illustrated with class components as follows:
class MovieBuzz extends Component {
render() {
const movie = {
name: "Deadpool",
director: "Tim Miller",
year: "2016",
};
return (
<div>
<MovieDetails movie={movie} />
<MovieRating ratings={this.props.ratings} />
</div>
);
}
In this example, two components are getting rendered: MovieDetails and MovieRating. However, any new MovieRating will result in a consequent re-render of the MovieDetails which does not necessarily need to change. To resolve this, rewrite the code as follows:
class MovieBuzz extends Component {
movie = {
name: "Deadpool",
director: "Tim Miller",
year: "2016",
};
render() {
return (
<div>
<MovieDetails movie={this.movie} />
<MovieRating ratings={this.props.ratings} />
</div>
);
};
Rewriting the same class component as above shows us that when MovieBuzz receives new props, the render function will not create a new constant for the movie. This action prevents the creation of a new props object that would force components to re-render unnecessarily. This scenario is simple, but you can imagine how much this technique would streamline a significantly bigger application with more components. Remember, rendering is different from updating the DOM. A component may be rendered without affecting any changes to the DOM.
Reusing Components
Components that can be used multiple times in an application need to be sufficiently generic. Unlike class-based components, functional components with hooks undergo a lifecycle without explicit lifecycle methods. This process allows for obtaining code reusability in custom hooks. However great this is, it is essential to note that purely functional components with hooks always re-render during a React render cycle. Therefore, React Hooks provide the best example for component reusability.
When a parent component renders, React recursively renders all of its child components. Let’s have a look at an example using the useState hook.
In an attempt to get away from the usual ‘counter’ and ‘timer’ naming conventions, this example of a buzzer has Loud and Soft components. The Loud component is the parent of Soft.
https://codesandbox.io/s/beautiful-morning-g8y64?file=/src/Components/Loud.js
On the first load, the page displays "Soft Buzzer" under the Buzzer button, as intended. Clicking the button sends a state update message causing the Loud component to re-render. By convention, the Soft component will also re-render. Since nothing has changed in the Soft component, there will not be any output. Therefore, this is an unnecessary render of the Soft (child) component.
An easy way to fix this would be to remove the Soft component from the Loud component, and in the App.js, make the Soft component a child of the Loud component. This fix is illustrated below:
To resolve this unnecessary child render, we must destructure the Soft component from props and include it in the JSX. This action renders the Soft component. Once this change is implemented, clicking the buzzer button on a clear console renders the Loud component.
Unnecessary renders occur when child components go through the render phase but not the commit phase. One way to fix this, as shown above, is pulling static or infrequently used components up into a parent (or even top-level) component.
Each custom hook affects the execution flow differently, as is explained in detail in the article, React Hooks - Understanding Component Re-renders.
Preventing Re-Renders: The Old Way
A traditional way of preventing excessive re-renders in class-based components is overriding a component update using the shouldComponentUpdate lifecycle method. This method takes in two parameters: nextProps and nextState. By default, when we call this method, the component re-renders once it receives new props, even though the props have not changed. To prevent the render method from being called, set the return to false, which cancels the render. This method gets called before the component gets rendered.
It looks something like this:
shouldshouldComponentUpdate(nextProps, nextState) {
if (this.state.buzz !== nextState.buzz) {
return true;
}
return false;
}
Sometimes you may want to prevent re-render even if a component's state or prop has changed. For example, when a component only cares about a piece of data nested inside the prop or state and the specific data has not changed.
Preventing Re-Renders: The New Way
Hooks have taken the React world by storm. This may sound cliché, but it’s a fact. One of the significant changes that hooks introduced to React is the potential to eliminate classes. React then introduced React.memo to facilitate better rendering in functional components, such as those with custom hooks. This introduction provided a new way to avoid excessive re-rendering.
React.memo is a higher-order component that optimizes functional components' performance by wrapping components when they render the same results given the same props. This component boosts performance by memoizing the render output.
Therefore, if your component props fail to change between renders, React will skip and reuse the last rendered result. It’s important to note that these new methods conflict with old ones. When using React.memo, shouldComponentUpdate no longer works on hooks-based components.
Affect React.memo in two different ways to improve app performance: use a single component as its only parameter or use a comparison function added as a second parameter.
Implementing React.memo Using a Single Component as its Only Parameter
We saw earlier how a React component re-renders even when the props have not changed. For instance, when a parent component renders, it causes the child component to render as well. To avoid this behavior, implement React.memo as a wrapper around the child component and ensure the necessary imports. With this method, the child only re-renders when props change.
Using the same naming convention as above, the basic use case is represented like this:
function SoftComponent({ buzz }){
return(
<div>
SoftComponent: { buzz }
</div>
);
Implementing React.memoUsing a Comparison Function Added as a Second Parameter
This involves handling callback functions props. In such cases, use extra caution when memoizing components that use props as callbacks as they may give different results on each render. For instance:
function SoftComponent({ handleClick }) {
return (
<div onClick={handleClick}>
SoftComponent
</div>
);
}
const MemoizedSoftComponent = React.memo(SoftComponent);
function LoudComponent() {
return (
<MemoizedSoftComponent handleClick={() => {}}/>
);
}
The above example will re-render each time the LoudComponent renders since the props will have changed. To fix this, use the useCallback function to return a memoized version of the callback that only changes if one of the dependencies has changed.
function LoudComponent() {
const onHandleClick = useCallback(() => {
// returns the same function when re-rendered
});
return (
<MemoizedSoftComponent
handleClick={onHandleClick}/>);
}
Next Steps
We now understand how re-renders happen, what causes them, and how we can reduce unnecessary re-renders. To prevent excessive re-rendering, move the expensive component to a parent component, where it will render less often, and then pass it down as a prop. If this is not enough, inject React.memo into the code for better performance.
To delve deeper into this topic, check out, One Simple Trick to Optimize React Re-Renders.