The first article in this series aims to provide perspectives around the problems and goals of front-end development. In part 1 we will be discussing how to think about Component Composition. In part 2 We will be providing concrete examples with Code.
Before we begin, we should think about what “good code” is. The code:
- does what we want (functional),
- easy to write (developer velocity),
- easy to read (maintainable),
- does it well (small, fast, and reliable).
The Problem? Unclear Spaghetti Code. We had to dig through all our files, not even knowing where to look just to edit the small part we cared about.
Components are the tools that were created in order to break up those small parts of our site.
- Smaller Pieces of related codes means that things are easier to find, read, and maintain.
- Components are also reusable: Faster Development (DRY Principle)
While it sounds good in theory, in practice, there are a lot problems that can pop up.
- Components themselves can become very complex. (How do we keep our code simple?)
- Our components aren’t very reusable. (How do we design reusable components?)
Article Road Map
In order to solve these problems, we need principles by which to design components. The following are the topics we will talk about.
- Separating Levels of Concern
- Thinking about Composition
- Designing for Re-usability
Separating Levels of Concern
Problem: How do keep our components from being too complex?
Complexity generally comes from having to deal with too many concerns. But while quantity is a contributing factor, it’s not always the case that less concerns is better.
For example, as discussed earlier, people have tried to split markup, styling, and scripting into three concerns for a long time. However, this separation actually makes code more complex. These are three different concerns, BUT, they’re highly related by the same goal: to create a single element/component. These three concerns are in a single level of concern.
And many tools have recognized this reality. Every framework primarily attaches actions directly to the markup (even vanilla HTML/JS has an attribute-based API for attaching scripts to markup). Vue uses single file components, bundling the markup, styling, and scripting of a component in the same file. Tailwind CSS uses utility classes to mix styling in with the markup.
It’s not switching concerns that makes coding difficult. It’s the switching between Levels of Concern.
Levels of Concerns
Organizational Level Concerns (high)
- How do we see the problem?
- What needs to be done?
- How do we delegate responsibilities?
Approach Level Concerns (middle)
- What smaller problems/goals exist?
- How do we approach reaching those goals?
Implementation Level Concerns (low)
- We know exactly what we want to do.
- How do we do it.
An organizational concern might correspond to a particular view in your blog application. At the highest level you need to provide the user with content, a way to navigate to other content, a way to share and read comments. A component that works at this level of concern might be a View component. The View component delegates these three tasks to three other components: the navbar (for navigation), the main area (for content) and the footer (for commenting).
Now the Navbar, footer, and sidebar all have respective concerns, but what type of concerns are they? The Navbar’s primary responsibility is for listing out the site structure. Whether or not we consider we decide to make this component concerned with organization, the approach, or the implementation depends purely on complexity. It could be just about implementation if it’s just an unordered list of links and there’s no smaller concerns to deal with. Maybe we want to have multiple levels of depth in our navigation. Maybe it makes sense to describe our approach to solving the problem by adding a child component to the Navbar that is responsible for displaying each sub menu. Want a carousel and video reveal? At this stage, it might make sense to also be organizational.
Why keep components at levels? Firstly it’s a natural cascade of responsibility. If I am looking at a child component’s behavior, I can always easily start higher and go deeper, component level by component level, and have a good understanding of what does what and how. This is in stark contrast with concerns organized by the type of task they do such as files in folders marked components, actions, reducers, styles, etc. Want to know where something is and how to edit it? Better know how it works in advance.
The main reason is that it keeps related problems together. When you are going to be editing something at one level of concern, you’re likely going to touching other things at the same level. Better keep them close. Possibly more importantly, these levels keep unrelated concerns out! Imagine having code at the View/organizational level like a scroll listener that controls something in the comment/footer section. Imagine looking at footer component and not being able to find anything that could produce the bug you found. That’s complexity.
Ultimately the purpose of describing levels of concern is to allow you to link your components to the structure of your problem. There are other types and levels of concerns too! But these are the three most common and useful when designing your code base.
Common Causes of Breaking Concerns
- Over modularizing: Sometimes one component might have three parts but they’re all simple. A good rule of thumb is that the Component Template, code, and styling each take up about 1 full page column of your screen maximum. Any more and you should consider breaking it up.
- Sticking too closely to a programming rule of thumb: The Flux architecture with props down, and events up, works in a lot of circumstances. But sometimes it doesn’t. Don’t force yourself up or down 5 layers.
Thinking about Composition
Problem: When should I take the extra effort to make a component reusable? And in what way?
When coding, there are generally two different types of code that we write. There’s application-agnostic code, and then there’s application-specific code. Things like small elements, helper functions, libraries are generally agnostic to the application they’re in. Consequently, they should be designed to be reusable. And then there’s logic that’s specific to the application you’re building. Stuff that probably won’t be ported anywhere else.
Here’s one approach to writing components.
Blocks, Compositions, and Views.
- Blocks are the general use components that you could use across any application. (eg. Button)
- Compositions are “application specific” components. (eg. News Feed)
- Views are the High Level Application Specific components that more or less represent the main area of a site/app. (eg. Home Page)
In general, Blocks are used to build Compositions, which are used to build Views.
Blocks → Compositions → Views
Blocks are small general use components that we can use anywhere and everywhere within our apps. We can build more complicated blocks out of our smaller blocks too. The key thing about blocks is that we have to design them to be reused in a general manner. Flexibility is key!
Examples of Blocks
- Button (Element Block)
- Columns (Layout Block)
- Data Provider (Resource Block)
- Animation Modifier (Modifier Blocks)
More on Block Types in the next Section
Compositions are things we make using blocks that are specific to a particular purpose. They can be reusable within your app, with slight differences between pages, but you should aim to make these general purpose. Only make abstractions when your app needs it.
- Facebook’s News Feed
- Your Custom Designed Post Card Component
Views often end up describing layouts and as such can be reusable in the sense that it can be used for many different contexts. Generally, however, you will extract out layouts as blocks.
The primary benefit of this type of composition is that it fits separating levels of concern. For example, with a View, you’re going to be primarily worried about organization. With a Composition, you’ll be thinking about approaches to behavior. Blocks will quite naturally do low level implementation: reaching into the DOM and using framework specific features like portals, two way data-binding, hooks, etc.
Next it helps organize your code base. Blocks will be cross platform shared component libraries. The root of the compositions folder will have all the High level Compositions, with their child components inside sub-directories. The Views folder can organize itself in a way that matches the routes.
Block Design: Improving Re-usability & Readability
The key to re-usability is flexibility. You will reuse a block when it fits your needs in a different circumstance. There’s generally a tradeoff between the amount a block does and how flexible it is. A block that does lots of things might be great for one situation, but might not necessarily for another. A block should do as much as possible but commit the user to as little as possible.
The two main things in we need to control in the front-end is how things look, and how they behave. In the React world, people try to do this by creating Presentational Components (how things look), and Container Components (how things behave). While a good split, most of the time, this generally isn’t granular enough to be flexible. Presentation, for example, has many sub-problems: context/markup, styling, positioning, etc. As such we will need to look at many other types…
We will only be covering several different types of components here, how they function within the context of thinking about composition. For concrete examples of how to design and implement blocks wait for Part 2.
Presentational Component Responsibilities
They are responsible for:
- Markup — The HTML that gets shown.
- Styling — The CSS for displaying the HTML
- Basic Event Handling/Basic UI State Management — Managing Basic State and actions such as Toggle, Hover, Click, etc.
Some people would say that you should try to keep your components purely functional. Only do it if it makes sense for critical performance. It will make your code more complex if you artificially make it functional.
Here are three types of presentational components.
- Visual Modifiers
Elements are the canonical block. Pieces of Markup that you can reuse everywhere in the app. They normally just wrap a real HTML element but have extra props for more useful customizability.
Layout Components are one strategy of structuring your components. Layout Components are, as their name suggests, just for laying out content.
While Layouts can usually be achieved with CSS class names which is the common approach with frameworks like Bulma or Bootstrap, if you want better encapsulation/want to stop css leaking into your layouts through global classes, using scoped styling within a component can work better.
Visual Modifiers are components you can use to wrap around other components to give them specific features like hover effects, animations, etc.
Container Components are the “logical” and behavioral components. They make use of lifecycle hooks, local state, state management, etc. In the context of blocks, they provide reusable patterns for state management, user interactions, or handling behaviors.
They are responsible for:
- State Management
- Data manipulation/Logic
- Data Communication
There are two major types of container components:
- Behavioral Modifier Components
- Resource Components
Behavioral Modifier Components
A Behavioral Modifier is a component that helps provide a small piece of functionality. They generally provide minimal markup/purely logical or are renderless components.
- Drag & Drop Reordering List
- Element Query/Visually responsive
A common pattern that pops up in front-end development is data management. There are going to be times where you will be getting and dealing with data with the same intentions. For example, you will need to:
- Get data from the server
- Create a loader/spinner
- Resolve Errors
- Display Data
Imagine having 4 different types of data resources. If you tightly integrated the logic with the views, you would have to rewrite the same logic 4 times (with slightly different parameters).
A resource component can be a nice way to communicate with your state management. Rather than rewriting “dispatch” actions for every component that needs to consume that data, you can have resource component do the heavy lifting.
Blocks are composable so obviously we can make more complex blocks out of smaller ones. You will often put multiple components together to form a small usable component such as tabs, or a toolbar. These preset widgets embody different UI patterns that make building complicated UIs easier.
Widgets often do a lot of things and are consequently generally not as flexible. One good strategy for creating reusable widgets is to the have default markup that a consumer of your block can overwrite later but still hook into the data and events of the component. Another is to make a lot of the features optional/hide-able.
Firstly, this is a long post! Congratulations for making it this far.
Key Take Aways:
- Keep code organized by concerns/problem
- Feel free to make components concerned about describing organization, describing approaches, and describing implementation.
- Consider organizing your components into blocks, compositions, and views.
- Improving flexible composition is about understanding sub-problems/things that a user might want to change from project to project.
In the follow up to this part on Composition, we dive into real life examples and patterns.
P.S. This is my first time writing since college so any feedback is greatly appreciated.