Structuring our Styled Components | Part II
Building Composable Components
In a previous post, we discussed how our team organizes lower-level tiers of our UI by using Blocks, Elements, and Modifiers. This section will focus on Components, and Part III we’ll cover Compositions. But before we hop in, let’s review the overall structure:
- Compositions: responsible for handling application-level data and logic
- Components: responsible for handling UI logic and rendering Blocks & Elements
- Blocks: responsible for rendering their child Elements and their own context (logic-less)
- Elements: responsible for rendering themselves and handling their modifiers (logic-less)
- Modifiers: responsible for modifying Block & Element styles in a predictable manner
Rigid Component APIs
As mentioned above, Components are responsible for UI logic and rendering Blocks and Elements, and they look like common React components. Here’s an example:
Above is a
Notifier Component. It’s responsible for rendering the notification‘s text and icon. The only logic it needs to manage is mapping the
type of message to a particular icon name. This example has a rigid API and markup structure. Let’s talk a bit about Component API considerations.
Component API structure can range from rigid to flexible, and both have benefits. Rigid Components require users to know less about the underlying markup structure. For example, our current
Notifier Component requires no knowledge of the underlying markup for its use:
Notifier doesn’t have a lot of internal variation, this type of API may be the correct choice. This Component might have various states and messages, but it will always render an icon on the left and text on the right. By making this API more rigid, we’re reducing the mental overhead for composing this component. Users don’t need to remember which icon name and color modifier map to each particular
type of notification, nor do they need to know about the markup structure underneath. All of those implementation details are hidden under the hood.
Reducing mental overhead improves Component consistency by making the right choices simple and removing opportunities for error. But the rigidity of this pattern can come at a cost as the component changes and evolves.
For example, let’s say we want some instances of the
Notifier Component to not have an icon. Or perhaps, we want some to render icons on the right instead of the left. Our current structure isn’t flexible enough to easily allow for that. This API leaves us with only a few options. We could:
- Add an additional prop and logic to hide or show icons
- Update all instances of this component with a breaking change
- Create a separate, very similar component
All of these options have their own pros and cons, but none of them feel like a great choice overall. Perhaps we could have structured
Notifier’s API to be more composable and found a more flexible option. Let’s try that out.
Note: The composable component API patterns used below are discussed in more detail in Kent C. Dodds’ Advanced React Component Patterns Tutorial and in Brent Jackson’s article here, both of which I would highly recommend.
We’ll use the React 16 Context API to pass our
type prop from our
Notifier Component to our
Icon Element via a
Provider. I’ll provide code snippets below, but if you’d like to see a working example, you can find a sandbox here.
Now we can use our
Notifier Component like this:
This version of
Notifier is a lot more flexible. Users can easily add an icon or position it left or right of the text. A little more knowledge of the underlying markup structure is required, but a lot of the details are still hidden. In general, this composable pattern has been the most durable for our team. It’s flexible enough to be reused in many different contexts, but it also hides a lot of the complexity of the Component’s details.
Thanks for reading! Using composable patterns for our components has been great for allowing flexibility of use and also ensuring UI consistency. I hope the ideas here are helpful for you and your team as well! If you enjoyed this post, you should check out our other articles!