Componentization is the process of breaking down an interface into small reusable pieces that can be recombined.
In this article, I aim to cover what are the main goals of that process, a set of basic rules that can be applied, how to organize the different elements, and how deep we can go into that process.
What is a component?
According to dictionary.com, a component is:
a constituent part; element; ingredient.
The concept of component, understood as constituent element, has been around in the browsers from the beginning of the Web. Let’s take as an example a very common form control in its simplest possible configuration.
<select> <option>Option 1</option> <option>Option 2</option> <option>Option 3</option> </select>
Rendered in the browsers, without any CSS, it looks like this.
Basically, with a couple of tags written in a certain order and number, we can render a native dropdown with few options. We do not need to worry about the events when expanding the dropdown, selecting an option, or typing, as well as about the internal scroll and status that keeps the selected value. All that implementation is managed by the browsers, hidden for the developers who are just consuming the
select native element.
Similarly, other built-in elements like
form, etc. have their own implementation and behavior.
Thus, a component has these qualities:
- Renders something on the screen.
- Can optionally have child elements.
- Has the implementation details hidden for consumers.
- Has associated styles.
- Can be instantiated several times.
- Can have an internal status.
Understanding what a component is will help us in the componentization process.
The concept of component is really powerful. It allows us to think of an interface built out of nested elements, in opposition to the concept of partials, big reusable areas, that we used years ago as it was a feature in many templating systems.
With that in mind, instead of big areas, we can build smaller components with clear responsibilities. By combining them properly, they will produce the interface we want. The ability of nesting elements is the real game-changer here.
However, facing the componentization with nested elements is way more complex than the process of breaking down the site into big partials. Componentization will thus produce more unit elements but with less code, as reusability increases, and at same time cleaner, more predictable, and testable.
The first step I usually do before starting the componentization process is trying to have the team aligned in few things:
- Understanding the requirements. It is important for the team to be aligned in the project vision and goals, as that might change the decisions.
- Defining your rules. Different projects and teams may require different approaches. Defining the rules is key, so everybody involved in the project can stick to them.
- Componentization should involve all the teams from the beginning. Breaking down an interface assumes that it is already built when starting to think about components. I always like to challenge that as the interface should be planned as reusable elements from its creation in interaction and visual phases.
Once the team agrees, these are the steps in the componentization process:
- Find common patterns. Once the interface is done, finding common patterns in the visual design will produce a bunch of elements that can be reused.
- Merge the patterns when possible. Some patterns might be not the same but quite similar. Does it make sense to merge them in a single component with a modifier or is it better to keep them separated?
- Be flexible. A small change in the previously defined visual design can decrease the complexity considerably.
- Make sure all the interface is covered by those patterns. Do not leave parts of the interface without defining an associated component for them. Some components might be used just once, but that does not mean they are not reusable.
- Think about the future. The decisions taken today need to be future proof as your application will evolve.
- Keep track of the new ideas, test them, and consolidate them if they prove to be good, so they can be implemented again in future projects.
Componentization rules and scoping
This is probably the most complex aspect in the process: defining what the rules are and how the team should make the decisions. I have seen many failed developments because the rules were not clear, or there weren’t rules at all. Here I will describe those rules and concepts that work for me and my team.
Let’s start from what is, in my opinion, the core definition of the process. What is a UI component:
A UI component is the minimum reusable element of an interface.
As it might look like a rather simple definition, it includes a couple of key ideas. Minimum means that a component can not be broken down into smaller meaningful pieces. Reusable means it needs to solve a single problem, it needs to have a purpose and a meaning. A component should be small enough to solve a problem, but not too small to be a non-standalone part of a bigger solution.
A UI component, then, must have a unique responsibility and we, as developers, must be able to verbalize it.
Examples of components are logo, button, menu, hamburger, breadcrumb, input text, dropdown, video, image… any small and standalone piece of the interface.
Building components isolated from the environment
Since the components are rather small pieces of an interface, we will need higher-order components that can import few components and use them. I call those higher-order component
I understand components as independent objects to encapsulate some styles and behavior. Black boxes for parent
modules that can only send them props, but ignore what happens inside them. And the other way around, components should not be aware of where they are going to be used.
A good practice then is building components isolated from the rest of the site, so the developer can focus on a single component without environmental constraints. Tools like Storybook help to accomplish this.
As a general rule, it is a good idea to avoid that components position themselves. Properties like
position: fixed, or any other combination of props that will place the root of a component will condition later how any parent
module can import that component and use it.
Following the same principle, it is also good to avoid having external margins that could push other components.
Components can get properties: values that are passed from the parent scope. Similar to abstraction in OOP, the set of properties and their types is the component API.
When designing a component’s API these are the rules I try to follow:
- Every property must have a purpose. And the developer should be able to describe it.
- The essential properties have to be required, while the non-essential props can be optional. For example, in a video component, the
videoSrcprop would be required, while the poster image or the description could be optional. A key part of the API design in every component is identifying these essential props.
- Flatten the props. It is a good idea to try to avoid complex objects inside a single property when possible. Instead of that, several properties with simpler structure will make the component easier to understand, consume and integrate with tools like a CMS later on.
- Limit the values when possible. If the prop works with a limited set of values, setting them as the only acceptable ones will help when consuming, and documenting the component.
- Avoid duplicating or collapsing the props. The component should never be broken when all the props are set at the same time, so in complex components controlling all the possible combinations is key to build a solid element.
Types of components
I usually distinguish between three types of UI components: Finals components, containers, and layouts.
Final components are those which don’t accept child elements. They are at the end of the tree and they are usually simple to identify and develop as they do not need to be taken into account in the nested structure.
Examples of final components are a menu, a logo, a video, a hamburger, any form element…
Container components by definition accept nested children but they do not know anything about them. Container components never style or change child behaviors.
Examples of containers: any kind of section, a modal, a panel, a tabs structure…
Page components are the root component of a website. Typically, a project will have only one, maybe two in some special cases. The page component manages the main layers of the site without knowing what they contain: sticky header, side panels, content, footer…
A modifier is a flag that can change the default component behavior or appearance. With appropriate modifiers, a component with a single code base can cover more than one case, increasing reusability and reducing the complexity.
I usually approach the modifiers as mutations of three types:
A variant is a mutation that changes the component appearance right from the instantiation. Typically it refers to changes in props like
A state modifies the state of an element after a user action is performed. For example, the
expanded state in an expandable box, the
cross state in a hamburger button, a
collapsed state in a sticky header…
A context is a variant or a state applied in a parent component, which triggers some different appearance in the child component.
In my article Component mutations: an alternative to base-modifier class approach - part 2 there are examples of mutations and an approach to coding them in SCSS.
CSS architecture to avoid dependency hell
Components are the base of an interface as they are the building units, our bricks. But they are not enough to build an entire website. They need to be nested and combined to produce complex interfaces.
Nesting can be done without adding any dependency to the parent component, as it is just receiving children via slot (or children property). But combining two or more components in a parent element does create that dependency since the parent needs to import the children to consume them.
Hence, a component can eventually import other components, that at same time could import more, creating a dependency tree that can be very complex to manage.
To ease the dependency graph, I usually add two more levels in the architecture. Those levels also contain components, but their purposes are different. Having multiple levels, allows us to avoid dependencies between elements in the same level, and thus make the dependencies go in one direction.
- Basics are purely structural components: wrapper, grid, cell, slider, and carousel. They can be extended to some other key elements like clickable, images, or videos if we want to add a11y capacities to them. I have been using web components for this layer, as it contains elements that are similar in all the projects with just a basic style setup layer of custom props.
- Components are our units. A component should not have another component as a dependency, but they can use basics.
- Modules are component aggregators. They are usually big areas of the site. They import structural basics and components, creating a direct dependency, to build bespoke views. A module style should never, as a general rule, change the styles in a component as they are black boxes, but it can position them. A set of modules together should produce pages.
In some other articles, I will dig deeper into the architecture system and how to connect different pieces.
- A component is an interface element. Its implementation is hidden.
- Modern frameworks are based on that component concept.
- The ability of nesting components helps the developers to build complex interfaces.
- Involving the interaction and visual design teams in the componentization process will improve the process and the outcome.
- A UI component is the minimum reusable component. Needs to be standalone and can not position itself.
- The component API, its properties have to be carefully designed to make it easy to consume while remaining useful.
- Components can be containers (accept children) or finals (do not get children).
- Modifiers will help to cover more cases with the same code base.
- Depending on your goals, a more complex architecture with several levels can be necessary to improve the code.