Skip to main content

Command Palette

Search for a command to run...

Choosing the Right Path: Composable vs. Configurable Components in React

Updated
•8 min read
Choosing the Right Path: Composable vs. Configurable Components in React
T

I help product teams build quality software and lead engineering efforts. Currently working at OpenSpace as a Senior Software Engineer.

There's a topic I keep revisiting that I believe is crucial for writing and maintaining React applications: structuring UI components. There are two main approaches: composable and configurable components.

Let's explore the strengths and trade-offs of these approaches, and why you might prefer to actually only use one of these two types for most of your components.

🚨 Be warned, this post is highly opinionated.

Example: Alert Component

For the purpose of this article, we'll take a look at a small alert component with two example implementations. Let's say our application need alert components that meets the following requirements.

  • It displays a title and a description.

  • It has one of four statuses: success, error, warning, or info.

  • Depending on the status, it shows a different icon and color.

Here's what this component might look like.

Let's compare how our component might be used in both its configurable and composable versions. I'll focus mostly on usage rather than implementation for a crucial reason: for any reusable components, the API is often more important than the underlying code.

You can view the example implementation here.

Configurable components

Configurable React components have the following characteristics:

  • DRY (Don't Repeat Yourself): These components often reduce code duplication by encapsulating common patterns and behaviors.

  • Complex implementation: As they need to handle various use cases through configuration, their internal logic can become intricate.

  • Strict output control: They provide more predictable results, allowing developers to tightly control the component's output. This can be advantageous for consistency but may limit flexibility.

Using such a component might look like so.

<Alert
  status="success"
  title="Success"
  description="Your action was completed successfully."
/>

Composable components

The attributes of composable React components are:

  • Simplified implementation: Composable components tend to be smaller, have a simpler implementation, making them easier to both read and write.

  • Leveraging React composition mechanism: These components take advantage of React's built-in composition features, aligning well with React's core design principles.

  • Less control over the outcome: It allows for more flexibility in how the component is used, but may also lead to less predictable results in some cases.

Using such a component might look like so.

<Alert status="success">
  <AlertIcon />
  <AlertContent>
    <AlertTitle>Success</AlertTitle>
    <AlertDescription>
      Your action was completed successfully.
    </AlertDescription>
  </AlertContent>
</Alert>

How components change over time

We've successfully implemented the alerts and are happily using them in our application. It turns out, however, that we will need to show more alerts in new parts of the application, but they need to be slightly different.

Let's revisit our example. Suppose we need to implement new alerts with additional requirements on top of the existing ones:

  • Dismissible alerts: Alerts display an additional button to dismiss

  • Optional icons: Not all alerts require an icon

  • Action buttons: Alerts can display an additional action button with a label and an action

Here’s how it might look like.

Now, let's see how the APIs of our example components need to evolve to accommodate these new requirements.

Configurable component

Configurable components grow by expanding their configuration—introducing more props. Here's how our API might evolve:

<AlertConfigurable
  status="success"
  title="Success"
  description="This alert has all features enabled."
  showIcon={true}
  dismissible={true}
  onDismiss={() => console.log("Alert dismissed")}
  actionLabel="Take Action"
  onAction={() => console.log("Action clicked")}
/>

We've added several new props: two for the dismiss button (one to indicate if the alert is dismissible and another for the dismiss click handler), one to control the icon's visibility, and two for the action button (label and click handler).

Composable component

In contrast, the way composable components evolve is, most often, by having more components added into the mix - either by splitting existing ones or creating new ones. Here’s how it might look like.

<Alert status="success">
  <AlertIcon />
  <AlertContent>
    <AlertTitle>Success</AlertTitle>
    <AlertDescription>This alert has all features enabled.</AlertDescription>
    <AlertAction onClick={() => console.log("Action clicked")}>
      Learn More
    </AlertAction>
  </AlertContent>
  <AlertDismissButton onDismiss={() => console.log("Alert dismissed")} />
</Alert>

We need to create two more components - one for alert dismiss button and one for alert action button. Their APIs will cover requirements relevant to these elements. Optional icon we get for free - if you don’t want this element, just don’t render it.

So, which one should we choose?

Your component implementation doesn't matter as long as existing alerts remain unchanged and new ones follow the initial requirements. However, in large-scale applications, the odds of this happening are, in my estimation, lower than winning the lottery.

I've worked on numerous projects of various sizes. In my experience, regardless of the project's size, scope, or initial assumptions, there's always one more case to handle. There's always that one page that needs to be different, that one component instance we want to tweak, or that one user flow that escapes the initial requirements.

We already went through one round of changes—now imagine we go through a few more iterations. What happens to the configurable component?

Apropcalypse

There is a funny term called apropcalypse, coined by Jenn Creighton, to describe components with dozens of props. This happens when there are too many props to cover all possible configurations and ensure reusability, but instead, it makes the component inflexible and leads to a cluttered API.

This is what configurable components often turn into, as you can see in our example—we made only a few changes, the number of props increased to 8.. We tend to have a just one more prop mindset when making changes—it's often easier to add to an existing abstraction rather than rethink the original design and potentially break it up into smaller pieces.

Speaking of abstractions...

Patterns, patterns everywhere

Configurable components can provide more immediate value due to their opinionated nature. They're often quicker to develop with, at least initially. You can create a single component that encapsulates an observed pattern, use it wherever needed, and adjust props to achieve the desired behavior. It's fast and effective.

However, the question remains: Is the pattern you've observed truly a pattern? How do you know? With composable components, you don't need to answer these questions at all. The trade-off is more code—often repeated code—but that's by design. You're allowing each piece of UI using this component to evolve independently in the future. There's value in this flexibility, though it's not immediate. It's the value of adaptability to change.

A word on DRY

This concept directly relates to DRY—Don't Repeat Yourself. Having two pieces of code look similar isn't sufficient reason to abstract them. You need one more crucial piece of information.

The key question is: Will these code segments change together in the future? If you have strong evidence for that, then by all means, create an abstraction that expresses this relationship. This is where more opinionated, configurable components truly shine.

The challenge lies in answering that second question—it's difficult, if not impossible, to predict the future with certainty.

I highly recommend reading Swizec's insightful post on this topic.

Optimize for change

Composition is at the heart of building applications with React. It's one of the main reasons why React has become so popular.

Design systems and component libraries are a good example of this. Since the exact use cases for the components are fundamentally unknown to the library providers, they need to optimize for extensibility and composability. There is very little certainty about how these components will end up being used and arranged together. As a result, design system libraries tend to be built heavily relying on composable components.

But your application code is in a components library. The number of use cases you need to support is not infinite—it's probably just a few. So, you might wonder, why bother creating smaller, composable components?

To start, these components are fundamentally optimized for change, which is one of the signs of a good API. They utilize React's composition mechanism, aligning with the framework's nature, and demonstrate that JSX is simply the right abstraction for most cases. If it works for most of the industry, it's likely a good fit for the UI you're trying to implement. I would say make sure you have strong reasons if you decide to do something different.

When you build your components to be composable, changes usually require less work (or sometimes come for free, like in our example)—both in implementation and regression testing. Implementation is simpler because you’re changing smaller pieces at a time and don’t have to deal with many dependencies. Regression testing is also simpler because instances of the components are more isolated.

It's perhaps easier to justify composable components for low-level UI elements like buttons, form elements, menus, or—as in our example—alerts. These need to be flexible because they're used frequently. However, change isn't limited to low-level UI elements—it happens across all levels.

With that in mind, I believe composable components are your best bet.

What about using both?

There’s a case for having both composable components and configurable versions of components and use the right one depending on the use case. Here’s how you could approach this.

  • Create composable components.

  • Create opinionated configurable components that use foundational composable components.

  • Use configurable components for the most common scenarios.

  • For more complex scenarios, create custom implementations using composable components instead of extending configurable ones.

Where this approach gets tricky is avoiding the just one more prop tendency and not extending the configurable components' API over time. Additionally, it requires everyone to stick to this approach and have a shared understanding of when to use a configurable component and when to switch to a custom, composable implementation.

When done correctly, we get the best of both worlds: composable components provide flexibility, while configurable components offer quick implementation. However, in my experience it's not easy to achieve this with real projects that have many engineers working in parallel.

Conclusion

That’s it! We discussed how to structure components in React using two approaches: composable and configurable components. We also explored how these components might change over time.

Configurable components provide immediate value and strict control, but they can become cluttered and inflexible as requirements change. Composable components are more flexible and easier to adapt in the long run, especially in large-scale applications.

This flexibility is worth optimizing for, so I recommend choosing composable components for better adaptability, using configurable components only when there's a strong need. Understanding these concepts will help you make better decisions when designing your components.

Further reading and references

More from this blog

T

Tomasz Gil - Software Engineer | Blog

53 posts

I help product teams build quality software and lead engineering efforts. Currently working at OpenSpace as a Senior Software Engineer.