Reasons to Avoid Nested Components
Introduction
During a project, I encountered an issue where unnecessary re-renders were occurring.
The problem was not just rendering, but the fact that the DOM was being completely redrawn with each render. The culprit was the use of nested components in the code.
In this article, I will explain why you should avoid using nested components.
Cheat Sheet
// Bad
const List = ({ hasWrapper, borderStyle, children }) => {
const Border = () => {
return <p style={borderStyle}>{children}</p>;
}
if (hasWrapper) {
return (
<Wrapper>
<Border />
</Wrapper>
);
}
return <Border/>;
};
// Good
const List = ({ hasWrapper, borderStyle, children }) => {
if (hasWrapper) {
return (
<Wrapper>
<Border borderStyle={borderStyle}>
{children}
</Border>
</Wrapper>
);
}
return (
<Border borderStyle={borderStyle}>
{children}
</Border>
);
};
const Border = ({ borderStyle, children }) => {
return <p style={borderStyle}>{children}</p>;
}
As shown above, you might use a nested component to reuse the code for Border and the props of List. However, in such cases, the children are unnecessarily repaint in the DOM with each render.
Problem Case
import { useEffect, useState } from 'react'; export default function App() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { setCount(count + 1); console.log('update count: ', count + 1); }, 5000); }, [count]); return ( <List count={count}> <img src="https://picsum.photos/200/200" /> </List> ); } export const List = ({ count, children }) => { const Border = () => { return <div>{children}</div>; }; return ( <ul> <p>count: {count}</p> <Border>{children}</Border> </ul> ); };
The problem case uses the same image URL to display different images https://picsum.photos/200/200 to check if the DOM is redrawn each time.
When looking at the code, the App
component increases the count every 5 seconds, which does not affect the List component. However, it is natural for React to re-render the children.
What is noteworthy here is that the DOM is being repaint. React uses the Virtual DOM to skip browser paint if there are no changes. However, the example code shows otherwise.
The cause is the Border component. Because Border is used as a nested component, it creates a new result with each render, and since the children are newly created, React doesn't recognize the DOM as the same and updates it.
export const List = ({ children }) => {
// Every render, children is newly created.
const Border = () => {
return <div>{children}</div>;
};
return (
<ul>
<Border>{children}</Border>
</ul>
);
};
Resolved Case
import { useEffect, useState } from 'react'; export default function App() { const [count, setCount] = useState(0); useEffect(() => { setTimeout(() => { setCount(count + 1); console.log('update count: ', count + 1); }, 5000); }, [count]); return ( <List count={count}> <img src="https://picsum.photos/200/200" /> </List> ); } export const List = ({ count, children }) => { return ( <ul> <p>count: {count}</p> <Border>{children}</Border> </ul> ); }; export const Border = ({ children }) => { return <div>{children}</div>; };
The resolved case declares the Border component outside of the List component.
export const List = ({ children }) => {
return (
<ul>
<Border>{children}</Border>
</ul>
);
};
// Border is not re-render by List's render.
export const Border = ({ children }) => {
return <div>{children}</div>;
};
Prevention
To prevent the accidental use of nested components, using ESLint is a good option.
react/no-unstable-nested-components rule checks for this.
{
"rules": {
"react/no-unstable-nested-components": "error"
}
}
The reason for using this rule is exactly what is explained in this article. If you have the curiosity, I recommend reading the content described in the rules as well.
Conclusion
While writing code for a project, I overlooked avoiding the use of nested components, which led to a problem. To reduce code duplication and avoid declaring multiple props, I declared a nested component inside another component, which caused the issue of the DOM being redrawn.
I only realized the DOM was being redrawn after enabling Chrome DevTools and invalidating the cache, which suggests it's an easy problem to overlook. I've learned about a pattern to avoid in React, and with the ESLint rule settings, I can prevent similar issues in the future.