Photo by Oskar Yildiz on Unsplash
Efficient React Components: How to Prevent Unnecessary Re-Renders with Props and Children
In this article, we'll explore two simple yet powerful techniques to help you avoid unnecessary re-renders in React. By applying these techniques, you can optimize the performance of your React application and ensure that your components only update when necessary.
#1 Leveraging Components as Children
In React, when a parent component updates its state, it triggers a re-render of that component as well as all of its child components, their children, and so on. This can sometimes result in unnecessary re-renders and impact the performance of your application.
It's common to have a component with state logic that also contains other components which don't depend on that state. For instance, consider a component that stores user data and renders two other components: HeavyApiCalls
and SlowContent
, which can be resource-intensive.
const Section = () => {
const [userData, setUserData] = useState('');
return (
<div>
<div>
/* these elements trigger state updates */
<input onChange={(e) => setUserData(e.target.value)} />
<button onClick={() => setUserData('')}>Clear</button>
<Search userData={userData} />
</div>
/* re-renders happen for these components */
<HeavyAPICalls />
<SlowContent />
</div>
}
Unfortunately, the heavy and slow components are also being re-rendered every time the user types in the input or clicks the button. This can negatively impact the performance of our app.
Thankfully, we can optimize the component architecture by leveraging the fact that children
is treated like any other prop in React. There's an important rule we should remember: Props are not affected by state changes in a component.
We can take advantage of this rule by rendering both the HeavyApiCalls
and SlowContent
components as **children
**of our section. By doing so, we can ensure that they won't re-render unnecessarily when the parent component's state changes due to user input or other events:
const Section = ({ children }) => {
const [userData, setUserData] = useState('');
return (
<div>
<div>
<input onChange={(e) => setUserData(e.target.value)} />
<button onClick={() => setUserData('')}>Clear</button>
<Search userData={userData} />
</div>
{children}
</div>
}
const Container = () => {
<Section>
<HeavyAPICalls />
<SlowContent />
</Section>
}
Now, you can trigger as many state updates as you need from the Section component without causing unnecessary re-renders of its children. That's pretty neat, right?
It's important to keep in mind that, at the end of the day, children
are just regular props in React. By treating them as such, we can optimize our application's performance and prevent unwanted re-renders with the right component architecture.
#2 Using Components as Props
This is a technique similar to rendering components as children, but it's even more flexible, especially in use cases where rendering everything as children isn't possible. Consider the following example:
const App = () => {
const [theme, setTheme] = useState('light');
const updateTheme = () =>
theme === 'light' ? setTheme('dark') : setTheme('light');
return (
<>
<header>
<h3>Welcome user!</h3>
<UserApiInfo />
<button onClick={updateTheme}>Change to {theme} mode</button>
</header>
<main>
<h4>News feed content</h4>
<BigList />
</main>
<HeavyFooter />
</>
);
}
In some cases, we can't use the children
prop to prevent unwanted re-renders, especially if the components we want to exclude are spread across our main container component.
For instance, consider the example where we have three components - UserApiInfo
, BigList
, and HeavyFooter
- that we don't want to be affected by state changes since they're either performing API calls or are slow and resource-intensive.
Fortunately, there's a solution to this problem that's based on the same rule we discussed earlier - that props aren't affected by state changes in a component.
We can pass all these components as props to our App component and refactor the implementation accordingly. Here's how we can do it:
const App = ({ header, main, footer ) => {
const [theme, setTheme] = useState('light');
const updateTheme = () =>
theme === 'light' ? setTheme('dark') : setTheme('light');
return (
<>
<header>
<h3>Welcome user!</h3>
{header}
<button onClick={updateTheme}>Change to {theme} mode</button>
</header>
<main>
<h4>News feed content</h4>
{content}
</main>
{footer}
</>
);
}
Finally, this is how the App component implementation would look like:
<App
header={<UserApiInfo />}
main={<ApiContent />}
footer={<HeavyFooter />}
/>
As we've seen, one of the key principles to creating a high-performing and well-architected component structure is to remember that props aren't affected by state changes in a component.
By leveraging this rule, we can prevent unnecessary re-renders of our components and improve the performance of our applications.
I hope that these techniques will help you optimize your React applications and avoid the pitfalls of unwanted re-renders. With these best practices in mind, you can create more efficient and effective components that deliver a better user experience.