When I first learned about React, I heard about all of its advantages: Virtual DOM is super fast, one-way data flow is very predictable, and JSX is... an interesting way to put markup into JavaScript.
But the biggest advantage of React is one I only got to appreciate over time: The ability to compose components together into more complex components.
It's easy to miss this advantage if you've always been used to it. Believe it or not, grouping component logic, styles, and markup together into a single component was considered blasphemy about a decade ago.
"What about separation of concerns?"
Well, we still separate concerns, just differently (and arguably better) than before. It's all about code cohesion. The styles of a button, the logic that happens when a button is clicked, and the markup of a button naturally belong together to form that button.
I don't think we do component composition enough, which is why many applications stop with component composition at a certain point and continue with its natural enemy: conditional rendering.
Consider a simple shopping list component:
export function ShoppingList(props: {
content: ShoppingList
assignee?: User
}) {
return (
<Card>
<CardHeading>Welcome 👋</CardHeading>
<CardContent>
{props.assignee ? <UserInfo {...props.assignee} /> : null}
{props.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))}
</CardContent>
</Card>
)
}
This is perfectly fine. But things get complicated when we start adding multiple states:
export function ShoppingList() {
const { data, isPending } = useQuery(/* ... */)
return (
<Card>
<CardHeading>Welcome 👋</CardHeading>
<CardContent>
{data?.assignee ? <UserInfo {...data.assignee} /> : null}
{isPending ? <Skeleton /> : null}
{!data && !isPending ? <EmptyScreen /> : null}
{data
? data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))
: null}
</CardContent>
</Card>
)
}
The solution? Early returns. This approach reduces cognitive load and makes the code more readable:
function Layout(props: { children: ReactNode; title?: string }) {
return (
<Card>
<CardHeading>Welcome 👋 {props.title}</CardHeading>
<CardContent>{props.children}</CardContent>
</Card>
)
}
export function ShoppingList() {
const { data, isPending } = useQuery(/* ... */)
if (isPending) {
return (
<Layout>
<Skeleton />
</Layout>
)
}
if (!data) {
return (
<Layout>
<EmptyScreen />
</Layout>
)
}
return (
<Layout title={data.title}>
{data.assignee ? <UserInfo {...data.assignee} /> : null}
{data.content.map((item) => (
<ShoppingItem key={item.id} {...item} />
))}
</Layout>
)
}
This approach is about both component composition and early returns. The key is to avoid conditional renderings for mutually exclusive states.
Always remember to go back to the drawing board when your components start feeling complex. Break down your UI into logical components, and don't be afraid of a little duplication if it makes your code more readable.
Let's build smarter, more maintainable React applications!