On composable, modular frontends

A look at using composable design for frontend components to help build scalable and maintainable frontend systems.
Part of
Issue 13 May 2020

Frontend

Think about your favorite retailer’s e-commerce platform. There’s a lot of complexity in that application: the product catalog, the cart module, the checkout page, the review system, the shipping tracking page, and so on. Each component of the web application has a distinct set of features, but they all connect in some way. The cart module is tied to the checkout page, and the product catalog is tied to the review system. So, how do e-commerce sites manage this complexity in their frontend applications?

Composability and modularity are appealing design principles for engineering systems. When employed properly, they can supercharge the development process and ease the maintenance burden of software. But they can be rather difficult to implement, especially as systems age and new business requirements are introduced. While these principles are often considered in the design of backend components, they’re equally relevant in the frontend.

With the growing intricacy of user experiences in different clients and an increasing set of frontend technologies, modern frontend engineering is a complex and sophisticated landscape. As clients do more heavy lifting in web applications and more state is stored on the client side, state machines increasingly manage more and more scenarios, such as the contents and status of a registration form. Furthermore, web applications need to support a variety of targets across mobile and desktop. With great complexity come great bugs and even greater maintenance burdens—but there’s a solution for this.

To build scalable and maintainable frontend systems, we need a strategy for managing and organizing the complexity that exists in the user interface. Scalable and maintainable systems are easier to test, reduce the cost associated with adding new features to an application, and make it easier to upgrade and change certain parts of the UI without degrading the experience for others. Thankfully, a growing number of technologies, such as React, Redux, Vue, and more, are making it easier for engineers to build frontend systems that can scale to meet their needs. But frameworks alone won’t solve these problems. We also need to bring the principles of composable design to the frontend.

The what

The principles of composability and modularity are often interlinked. Modular systems consist of subcomponents with well-defined interfaces and functions that can be consumed independently of each other. Composability is the degree to which these subcomponents can be combined to form more complex systems. Despite the similarities in these definitions, a modular system isn’t automatically composable. A system’s subcomponents can have well-defined interfaces and scopes but be difficult to integrate because data types aren’t generic enough across subcomponents, or because certain subcomponents may have unexpected side effects.

The why

When implemented in frontend systems, these principles offer many benefits to both developers and end users. For starters, it’s much easier to introduce new features or deprecate unnecessary ones in a modular system because the subcomponents aren’t deeply coupled; changes are much easier to isolate.

Composability and modularity also make it easier to maintain (and document) existing code. Instead of sifting through hundreds of source files to establish the relationships between different components in a system when making a feature change, an engineer only needs to examine one component of the system and understand the interface of others. You can imagine how much time that saves during both development and onboarding!

Last but not least, modular systems, in which components have limited scope and their internal complexity is obfuscated from other components, are much easier to unit test than monolithic systems. Since modular components are less likely to hold extraneous internal state and more likely to operate deterministically, there is less unexpected behavior and fewer side effects to account for when testing.

The how

The UI

Now, this is all fine and dandy, but how do you actually get it done? There are several fundamental elements to a frontend application to which we can introduce modularity and composability, including styling, event management, and DOM management.

For the purpose of this article, we won’t be prescriptive about what a component is; it could, for instance, be a React component, an Angular view, or a Vue component. We’ll focus on general principles that can apply to any rendering or UI library.

Let’s start off by covering something universal to all rendering libraries: styling. Styling frontends with CSS can quickly become a gnarly affair. Often, the styles applied to HTML depend largely on the structure of the page. In the example below, the nav element is aware of its position on the page and has a child button that’s aware of its inner content. This pattern doesn’t lend itself to modularity or style reuse.

<nav class='side-panel'>
<button id='settings'>Settings</button>
</nav>

nav.side-panel {
background-color: green;
}

button {
color: white;
}

button#settings {
font-family: Arial;
}

An alternative approach is to implement styles based on the intended effect in isolation and then compose styles by applying several effects on the same element, as opposed to within the context of an existing UI layout.

<nav class='green-bg'>
<button class='white-fg arial-font'>Settings</button>
</nav>

.green-bg {
background-color: green;
}

.white-fg {
color: white;
}

.arial-font {
font-family: Arial;
}

This pattern might ring a bell. It’s implemented by popular styling frameworks like Bootstrap and Foundation because they’re designed to be used across different frontend implementations.

In addition to avoiding context-dependent styles, composable CSS patterns avoid applying styles to tag-based selectors.

h1, h2 {
font-family: Arial;
}

The first of the three previous patterns is a global-level style that modifies all UI components within a page, as opposed to the approach in the second code snippet, which can be applied selectively.

Modularity requires subcomponents within a system to be easy to swap out or assemble in sequence. To do this, a composable frontend system should rely on native and standardized APIs, when possible. This makes the frontend system resistant to breaking changes in third-party APIs for fundamental logic such as manipulating the DOM: for example, relying on the getElementById method provided by the DOM API across browsers instead of the $(#id-here) accessor exposed in jQuery. Historically, developers had to rely on libraries to provide support for functionality that wasn’t standardized across browsers. However, with technologies like polyfills, more uniform support for APIs across browsers, and advancements in JavaScript language features, it’s more feasible for developers to rely heavily on standards.

Among the tougher elements to modularize on web pages are events that cause changes in different components. For example, a user might click on a button that will update a form field and submit a request to a backend API in a single interaction. Oftentimes, this is managed by integrating all of the related elements, the button and the form field, in one component and leveraging state to toggle the view or contents of each element. But this can quickly get out of hand. What if, in addition to updating a form field, we need to render a new UI element?

In lieu of adding more and more logic to our button component, we can implement a standardized interface for sending messages across different components within our page. In fact, the browser already supports this scenario via event listeners. Instead of coupling the logic into one component, the button element can dispatch an event that other components can listen and respond to.

The state

So far, we’ve covered styling and event handling, but there’s one thorny topic we’ve yet to discuss: state. Frontend applications store a lot of state, such as the contents of a user’s timeline or their current application settings. How do we structure the state of an application to improve modularity?

First, we can ensure that state is only associated with the components it’s related to. For example, a sign-in/sign-out button doesn’t need to access state about the contents of a timeline, so this information should be completely opaque to the button.

For scenarios in which state doesn’t need to be shared across multiple components, state modules can be implemented to manage the state for a particular class of components. The state module provides a set of getters and setters that each component can access and ensures that components cannot make conflicting or unexpected changes to the state.

Underpinning styles, state, and event handling is the structure of the frontend itself. This is often determined during the design of the user interface. To lend itself to modularity, the design of a web page should isolate related state into a singular component on the page. For example, the list of products a user recently purchased should be rendered independently of the list of products currently for sale. Also, user actions, such as liking a post or deleting a comment, should only be executed via a singular UI element.

The future

The principles of modularity and composability are codified in the implementation of micro-frontends. Micro-frontends are structures for creating frontends in which each component is isolated into its own application and rendered collectively on a page using mechanisms such as Web Components or iframes. While this approach maximizes the composability and modularity of a web application, it can be difficult to migrate existing frontends to this pattern. Furthermore, it takes a fair bit of work to integrate all of the subcomponents and ensure that they work properly. Leveraging some of the implementation patterns we’ve discussed provides a good migration store for existing frontends and strikes a balance between the technical benefits of modularity and the cost of migrating existing systems.

As the web advances and users demand more and more from the frontend experiences applications provide, we’ll need to design frontend systems with composability and modularity in mind. Favoring these design characteristics makes it easier to onboard engineers to existing frontend components—and to maintain and grow successful frontend systems.

About the author

Safia Abdalla is a software engineer working on open-source technologies at Microsoft and an open-source maintainer on the nteract project. When she’s not working on open source, she enjoys writing and running.

@captainsafia

Buy the print edition

Visit the Increment Store to purchase print issues.

Store

Continue Reading

Explore Topics

All Issues