Avoid React.Fragment in reusable components

Published
2021-02-03
Tags
ReactLayoutarchitecture

Good components are reusable. And reusable components don't make assumptions about where they will be used.

This means a few things:

1. Components should not have an effect outside of their boundaries. This is well explained in “Margins and Composability in CSS” by @giuseppegurgone and “Margins considered harmful” by @mxstbr.

2. Components should not implicitly rely on their context.

Recently, during code reviews, I noticed developers would return a React.Fragment from a component like this:

export function BigComponent() {
	return <>
		<Heading/>
		<Content/>
		<Footer/>
	</>
}

and then they would render it somewhere in the application like this:

<div>
	<BigComponent />
	<Button/>
</div>

This pattern has a few problems: it breaks component's encapsulation of styles and it makes your code much harder to change.

Respect Componentsʼ Boundaries

Returning a <React.Fragment/> from you render function will make BigComponent look differently depending on the context where its used and can lead to very obscure and hard to debug bugs related to styles. For example, if you add display: flex in the parent component:

<div style={{ display: "flex" }}>
	<BigComponent />
	<Button/>
</div>

By reading this code, youʼd expect that BigComponent and Button appear next to each other horizontally. In reality, though, Heading, Content, Footer, and Button will be laid out horizontally next to each other.

This is happening because BigComponent returns a list of 3 components wrapped into a <React.Fragment>. This results in the following HTML tree (pseudo-code):

<div style="display: flex">
	<header />
	<main/>
	<footer />
	<button/>
</div>

Applying display: flex to the parent div element affects its immediate children.

There is a simple principle to avoid such mistakes:

Components should always encapsulate their layout.

Based on this principle, the easiest fix could be simply wrapping all components in the BigComponent into a <div>

export function BigComponent() {
	return <div>
		<Heading/>
		<Content/>
		<Footer/>
	</div>
}

This change might look like an unnecessary one (we introducing an additional DOM nesting 😱) but it prevents this sort of issues. No matter where you going to render BigComponent, all its children are going to be contained in the div element.

Context-aware rendering

Now letʼs assume you are doing this on purpose because you want the layout of the BigComponent look differently under different circumstances. The simplest solution is to eliminate the BigComponent completely by inlining the child components directly:

<div>
	<Heading/>
	<Content/>
	<Footer/>
	<Button />
</div>
💡
If a component doesnʼt have the children prop (or similar prop that parametrizes the output), it is a good indication you might not need a separate component just yet. If there is shared code like data fetching etc, consider using simple functions or hooks.

Optimize for change

Inlining is the best strategy until you see some patterns emerge. For example, you'll see that in some cases the content must be rendered with a border around and in some without.

Adding the border manually to instances of the original BigComponent with the fragment is a tedious and error prone process. Encapsulating this behavior into a component is the way to go.

React already provides us with a perfect API for that — props. Letʼs parametrize our component and tell it to render appropriately in a more explicit manner:

export function BigComponent({ variant }) {
	return <div style={{ border: variant === "withBorder" ? "1px solid grey" : "none" }}>
		<Heading/>
		<Content/>
		<Footer/>
	</div>
}
💡
Be careful when modeling props interface and consider how the pattern can evolve in the future. Adding a boolean prop like withBorder might feel sufficient now but it can lead to a bloated API and impossible states in the future. Instead, use a limited set of options. Something like variant: "default" | "withBorder" is more resilient since you can add more options later without breaking the API.

This way, you can always change how the component should be rendered based on props and updating all instances can be done in one single isolated change.

Conclusion

Building reusable components isn’t easy but following a few rules and principles can save you a lot of time in the future.

  1. Avoid creating components early on (i.e. avoid premature optimizations). Don't be afraid to repeat yourself multiple times before a pattern emerges.
  1. When you see a pattern and it's time to create a reusable component, focus on the interface and your goals. Ask yourself questions like "How flexible or rigid the component should be?" and "Will it evolve in the future?".

And most importantly:

Do not make any assumptions on where a component will be rendered.

If we look back to the original example, it should become apparent that returning a fragment (or essentially a list of things) from the BigComponent violates this principle.