Cross-posted from https://imkev.dev/component-composition
One of the most misunderstood design patterns in a React application is the render props pattern and component composition. While this isn't a new or novel approach and has been promoted since at least 2017, five years later I still encounter opportunities where it should have been used instead of an inheritance hierarchy. Component composition is the technique of combining different modular components to construct another component.
Render Props
"But almost all components are made up of sub-components, so what's so special about component composition?" I hear you say. I distinguish between a regular component and component composition by passing one or more of the sub-components as props to the parent component. These props are known as render props and the most commonly used render prop is the children
prop which is included in React by default.
Let's take a look at an example.
import Title from "./Title";
export default function MyComponent({ title, text }) {
return (
<div className="container">
<Title title={title} />
<p class="text">{text}</p>
</div>
);
}
export default function App() {
return (
<div className="app>>
<MyComponent
title="Random title #1"
text="Lorem ipsum..."
/>
</div>
)
}
The component above might look fairly ordinary. A component MyComponent
renders a div
element and within it, there are two child elements. One being the <Title>
component and the other being a <p>
element. MyComponent
receives two props, the title
and the text
component, which it outputs or passes onto the sub-component.
Let's see the same component using the component composition pattern.
export default function MyComponent({ children }) {
return <div className="container">{children}</div>;
}
export default function App() {
return (
<div className="app">
<MyComponent>
<>
<Title title="Random title #1" />
<p className="text">
Lorem ipsum...
</p>
</>
</MyComponent>
</div>
);
}
In this example, the role of MyComponent
is reduced to creating a div
element and placing the children
prop within the div
. The parent component which calls MyComponent
is responsible for creating the <Title>
and <p>
elements. The generated HTML in these two examples is identical.
Single-responsibility principle
When I was still at University studying computer science, amongst the many practices we were taught, there were the SOLID principles. Without going into the merits of the SOLID principles, the S in SOLID stands for the Single-Responsibility principle and states (paraphrasing) that a class or function should only have one reason to change. A class or function should only have one role. I like that. It makes it easier to understand, easier to debug, and makes your code more portable.
The component composition pattern helps enforce this practice as the role of MyComponent
in the example above is to only create the div
and place the children
in the correct place. The role of App
is to construct the composition of different components required to build the module. Contrary to the first example, MyComponent
is not responsible for choosing the order of the <Title>
and <p>
elements, and if you would want to change their order, you would need to change MyComponent
. This violates the Single-Responsibility principle.
In practice
The above example is very simple and you are unlikely to encounter this scenario in a real-world environment. But the concept could be applied to any component structure.
In addition to displaying and outputting HTML, one of the more common tasks of a React component is to fetch data from a store or an API. Let's compare inheritance and component composition using an example where we are fetching a list of users and then displaying these in a <ul>
.
export default function UserList({ quantity }) {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(`${API_URL}${quantity}`).then(async (response) => {
if (response.ok) {
const { results } = await response.json();
setUsers(results);
}
});
}, [quantity]);
return (
<div className="container">
{users && Boolean(users.length) && (
<ul className="list">
{users.map((n) => (
<li key={n.login.username} className="item">
<UserCard
username={n.login.username}
city={n.location.city}
profilePicture={n.picture.thumbnail}
/>
</li>
))}
</ul>
)}
</div>
);
}
export default function App() {
return (
<div className="app">
<UserList quantity={3} />
</div>
);
}
The UserList
component receives a quantity
prop indicating the number of items to retrieve from the API. Once the component is mounted, it will make a request, populate the result in the state, and then display a list of UserCard
sub-components inside a <ul>
.
Let's take a look at the same application if it were following the component composition pattern.
export default function Users({ quantity, children }) {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch(`${API_URL}${quantity}`).then(async (response) => {
if (response.ok) {
const { results } = await response.json();
setUsers(results);
}
});
}, [quantity]);
return children({ users });
}
export default function App() {
return (
<div className="app">
<Users quantity={3}>
{({ users }) => (
<div className="container">
{users && Boolean(users.length) && (
<ul className="list">
{users.map((n) => (
<li key={n.login.username} className="item">
<UserCard
username={n.login.username}
city={n.location.city}
profilePicture={n.picture.thumbnail}
/>
</li>
))}
</ul>
)}
</div>
)}
</Users>
</div>
);
}
Component Composition Data Demo
The App
component now renders a Users
component. This component is solely responsible for fetching the users from the API and returning them as a prop to the children
using the return statement return children({ users })
. Any child component of Users
will have access to the users
as a prop. The App
component iterates through the users it receives and creates the <ul>
.
The latter approach allows you to separate fetching data from displaying it. If a change request comes in that requires the data to be filtered before being displayed, you immediately know that you do not need to do any changes to the Users
component as the change request does not require changes to the fetching of data. Demo
Multiple Render Props
While in many cases you can use the children
prop included in React, in some cases you may need to place multiple sub-components that will not be rendered beside each other in the DOM. In these cases, you may define further render props similarly to how you would assign an ordinary prop. The only difference is that you pass a component.
export default function MyComponent({ headerFn, children }) {
return (
<>
<div className="header">{headerFn}</div>
<hr />
<div className="container">{children}</div>
</>
);
}
export default function App() {
return (
<div className="app">
<h1>Component Composition</h1>
<MyComponent headerFn={<Title title="Random title #1" />}>
<p className="text">
Lorem ipsum...
</p>
</MyComponent>
</div>
);
}
In this simple example, we have added headerFn
prop to MyComponent
and passed <Title title="Random title #1" />
to it. MyComponent
is only responsible for the DOM structure and placing the correct CSS classes, while App
is responsible for defining the components to be rendered.
Conclusion
The component composition pattern can simplify your applications, making the code more portable, maintainable, and DRY. Once you become accustomed to this pattern, it is easy to apply it in almost any situation you would have previously used inheritance.
I hope this helped convince you to make use of component composition more often. If you're not yet convinced, ReactJS docs go as far as to say that they havenโt found any use cases where they would recommend creating component inheritance hierarchies and Michael Jackson has a fantastic video (one of many) on this topic Never Write Another HoC.
Thank you for reading. Have a good one! ๐
Image credits: Photo by Ricardo Gomez Angel
Top comments (0)