selbekk

Creating Composite Components

December 9, 2018
4 min read

Originally published at react.christmas

Do you need to create a complex, yet reusable piece of UI? What's the best approach?

When I work on large applications, I often stumble across usecases where a simple component just isn't enough. These pieces of UI tend to be the ones that are reusable, flexible to fit a ton of usecases, and end up with a multiple-digit number of properties.

My favorite example of this kind of component is a card component. It can contain images, text, titles, lists, icons - you name it!

Here's an example of a typical, flexible component that's hard to use:

<Card
    image="/static/images/happy-people.png"
    imagePosition="top"
    href="/favorite"
    text="The most watched movies, available in a heart beat."
    title="My favorites"
    type="primary"
/>

Tons of props, and it doesn't scale too well. What can we do?

Use children

The first enhancement we can do, is to place the most dominant piece of the content in the children prop - so that we at least feel like we're encapsulating our content in some sort of wrapper.

<Card
    image="/static/images/happy-people.png"
    imagePosition="top"
    href="/favorite"
    title="My favorites"
    type="primary"
>
    The <strong>most watched</strong> movies, available in a ❤️ beat.
</Card>

An added bonus is that using JSX feels more intuitive, too!

This, however, isn't enough. We still lack the flexibility we need to get rid of the rest of our pesky props! Time to do something drastic.

Creating sub-components

A typical approach when you find youself in such a situation, is to create what I call "sub-components". These components don't make much sense on their own, but they make up the individual parts of a larger component.

Our Card component, for instance, could be implemented like this:

const Card = props => (
    <CardWrapper type={props.type} href={props.href}>
        {props.imagePosition === 'top' && <CardImage src={props.image} />}
        <CardTitle>{props.title}</CardTitle>
        <CardContent>{props.children}</CardContent>
        {props.imagePosition === 'bottom' && <CardImage src={props.image} />}
    </CardWrapper>
);

If we just disclosed these sub-components somehow, and just let the consumer decide which order they should come in (and what props they should receive), we would be more flexible than a bungee cord.

Our component could then be used like this:

<Card href="/favorites" type="primary">
    <CardImage src="/static/images/happy-people.png" alt="Happy people" />
    <CardTitle>My favorites</CardTitle>
    <CardContent>
        The <strong>most watched</strong> movies, available in a ❤️ beat.
    </CardContent>
</Card>

That's pretty easy to arrange a ton of ways, right?

How to make these sub-components available to the consumer?

So we now have a main component, and a few sub-components. How do we expose our API to the consumers? The way I see it, we have 4 alternatives

Alternative 1: Default + named exports

Our first alternative is to expose the main component as the default export, and any sub-components as named exports. You'd use them like this:

import Card, { CardImage, CardTitle, CardContent } from 'components/card';

This works fine, but there's a few downsides too. You have to remember that there's both default and named exports, and you (most likely) need to prefix every component with the main component's name, in order not to litter the file-global namespace with imports. Still, it works just fine!

Alternative 2: Totally separate components

Keeping the sub-components as totally separate components might also seem like a good idea, but beware of other people using these components outside of their intended context too. There's little to no connection between your main and sub-components this way. Also - you'll get a ton of import lines!

import Card from 'components/card';
import CardImage from 'components/card-image';
import CardTitle from 'components/card-title';
import CardContent from 'components/card-content';

Alternative 3: Default export + components as static props

A pretty popular way to solve this is by only exporting the main component, and then exposing any sub-components as static properties on it. You use it like this:

<Card href="/favorites" type="primary">
    <Card.Image src="/static/images/happy-people.png" alt="Happy people" />
    <Card.Title>My favorites</Card.Title>
    <Card.Content>
        The <strong>most watched</strong> movies, available in a ❤️ beat.
    </Card.Content>
</Card>

This is the way I've been doing this for a while. Yep, you still need to remember that the properties are there (just like alternative 1), but static code analysis tools can help out. In addition, you don't have to think about littering the global namespace with imports!

Alternative 4: Render props

A last alternative, and my favorite right now, is using the render props pattern to expose the available sub-components. The usage would look like this:

<Card href="/favorites" type="primary">
  {({ Image, Title, Content }) => (
    <Image src="/static/images/happy-people.png" alt="Happy people" />
    <Title>My favorites</Title>
    <Content>
      The <strong>most watched</strong> movies, available in a ❤️ beat.
    </Content>
  )}
</Card>

This alternative has two very distinct upsides.

Firstly, we can skip the prefix and namespacing, since the sub-components only are available in the scope of the children function.

Secondly, we can make our components smarter! Based on the different props passed to the main component, we can expose different sub-components too - in addition to any extra flags or functionality we want.

The only downside I can think of is the syntax itself - if you're bothered by that kind of thing, of course 😉

Go split up your components!

No matter what you decide, I hope you try to split up your mega-chunks of UI into composable, flexible components with a few props each.

All rights reserved © 2024