One of my first projects after coming to Proton was to create a reusable card component. A relatively simple task, but with one catch. The card needed to dynamically hide certain UI elements based on the type of data provided. Not a problem, I thought: you can conditionally render those elements as needed. Unfortunately, this approach did not stand the test of time. As we added more card variations, the component got bigger and bigger, and more and more difficult to maintain and extend.

Enter composition. Unlike my original props-only approach, my new solution puts a focus on Children, building what you want instead of hiding the components you may not need. This may look like a distinction without a difference, so in this post, we’ll do a high level walk through of building a simple blog site with both approaches. Along the way, I’ll show why might use props and demonstrate how children smooth your path when props alone get unwieldy. Most importantly, we’ll explore the fundamental scaling problem with props and see why a composition-based approach avoids the pitfalls of conditional rendering.

Keep on Branching

To kick this off, let’s look at how a typical React site could be broken into components.

Diagram of a typical React site

Notice the application reduces component complexity each time it branches. We start with an application, which is made up of a header and the main content area. The header component takes care of a lot, so we break it into a NavBar and SearchBar. We’ll usually repeat this until the complexity is reduced to the point where we can happily program a given component within a single reasonably sized file.

How is this branching actually implemented? Through props and primitive HTML components like a <div>.

const Header = (props) => {
    return <div>
        <NavBar title={props.title} route={props.route} />
        <SearchBar/>
    </div>
}

For those less familiar with React, the annotations like title={props.title} within an HTML tag are what we call props. They let you take a piece of information, like props.title, and make it available to a child component as title.

Not So Pretty

The case for props starts to fall apart quickly when large portions of the UI are dynamic. Let’s focus on what the Body section of the aforementioned application could look like.

Diagram of a site that's too complicated

This diagram could describe a website that has three pages: A blogs page, a profile page, and a settings page.

Imagine we want the user profile page to have a banner across the top with user information (like their name and profile photo) and a footer across the bottom with your typical navigation and social media links. We’ll need a lot of other logic in this component to render the contents of their profile, like the blog posts they’ve written or comment threads they’re following.

Props will not save us here, because they aren’t equipped to hide components. At best, they can defer conditional rendering to somewhere else in our code. Let’s see what I mean:

// Version One
const Body = () => {
    return <div
        style={...} // apply some common styling to everything
    >
        {(route === 'blogs' || route === 'settings') && <UserInfoBanner />}
        {route === 'blogs'         && <Blogs />}
        {route === 'user-profile'  && <UserProfile />}
        {route === 'settings'      && <Settings />}
        {(route === 'blogs' || route === 'user-profile') && <Footer />}
    </div>
}

What we’re doing here is really straightforward: we check which route is active and render accordingly. The UserInfoBanner is shown when the user is on the user-profile or settings page and the Footer is shown if the user is on the blogs or user-profile page. The other three conditionals render the main content for their respective pages.

There are countless “micro optimizations” that we can do to make this slightly easier on the eyes, but as long as we’re not using Children, they will all face two fundamental scaling problems:

  • If there are M components to conditionally render, there will need to be at least M Boolean expressions that decide what is hidden and what is shown. In our case, we have 2: the UserInfoBanner and the Footer.
  • If there are N variations to display, each Boolean expression can contain up to N terms. In our case we have three: Blogs, Profile, and Settings.

In our toy example, M and N are quite small so the logic is manageable. As we add more pages and optional components, however, this Body component will balloon in size. Of course, we can keep splitting into subcomponents. But the amount of boolean logic in our app will never drop below M*N in a worst case scenario. If we had 10 components and 20 variations, that’s potentially 200 pieces to keep track of.

A Different Way: Using Element Children

The fundamental problem we’ve stumbled across is that decomposing a UI into components requires us to explicitly toggle all the different options that we can potentially use. What if we can instead define what we want and not even think about what we don’t want? This second children based approach does just that:

// Version Two
const WithCommonStyles = ({children}) => {
    return <div
        style={...} // apply some common styling to everything
    >
        {children}
    </div>
}

const BlogsPage = () => {
    return <WithCommonStyles>
        <Blogs/>
        <Footer/>
    </WithCommonStyles>
}

... // SettingsPage and UserProfilePage defined in a similar way

const Body = ({route}) => {
    if (route === 'blogs') return <BlogsPage />
    if (route === 'settings') return <SettingsPage />
    if (route === 'user-profile') return <UserProfilePage />
}

Let’s look at the WithCommonStyles component. In particular, notice how it uses a children prop even though we never pass in a children value when we use the component. Whenever you use the <WithCommonStyles> ... </WithCommonStyles> syntax, React automatically takes the ... between the tags and includes it as the children prop for the component to use. With this newfound power, we can inject content before and after the provided children. We can even modify props the children receive or access children individually!

Next, take a look at the BlogsPage component. In our example, we use the WithCommonStyles component to stitch together all the pieces for this single variation.

Finally, take a look at the Body. This component is now extremely simple. A lot of the work has been offloaded elsewhere. More impressively, the real logic that was happening in this section has been reduced to three conditional statements.

The overarching goal here is to split our components based on the variations we need to create. This allows us to make strong assumptions about what should and should not be rendered within the component, ultimately reducing our need to conditionally render. Conceptually, this can be thought of as starting with what we want and building it up with our data instead of starting with our data and breaking it down into what we want.

You might notice that there are a few more lines of code with this approach. Have we actually increased complexity? No: simplicity is more than lines of code. In this case, we achieve simplicity in our ability to more easily scale up complexity without getting lost. In our example, the number of logical branches is no longer coupled to how many components are being conditionally rendered. Instead, it only depends on the number of variations, which, in this case, is 3. As our app starts to contain more optional components, this design pattern will allow us to easily accommodate.

Recap

We started this discussion with the distinction between “building what you want” and “hiding the components you may not need.” I hope that by walking through this basic blog site example, this distinction became more clear and the benefits of composition more profound. However, the beauty of this pattern is that it can apply to any component whose variations are statically known! This pattern hugely improved Proton’s card components. Using composition in your own applications can net you these benefits as well. In addition to reduced component complexity, you’ll find your code leaner and more manageable, allowing you to quickly add new features without worrying about your existing ones.

Further reading: React: Composition versus Inheritance