This is the third part of the series „Condensed Angular experiences“. We will explore concepts in Angular that seem simple but are very powerful and critical for the application architecture.
- Understanding Angular’s Async pipe
- What is the hype with Angular’s OnPush change detection?
- Interlude: About smart and presentational components
- Smart and presentational components with Angular
- Different approaches to complex and advanced forms in Angular (coming soon)
You may have heard or read about this topic at some point. It is known under multiple names, but they describe the same or a similar architecture concept in the end.
Those different names include:
- Smart & Presentational Components (the naming we use in this article)
- Smart & Dumb Components
- Stateful & Stateless Components
- Stateful & Pure Components
- Container & Presentational Components
Since everything revolves around components in this article, let’s take a deeper look at what a component is and what we should keep in mind when writing such.
Understanding Angular’s Async pipe What is the hype with Angular’s OnPush change detection? Interlude: About smart and presentational components Presentational and Smart Components with Angular Different approaches to complex and advanced forms in Angular
This is the first article explaining this architectural concept in a set of two. This part discusses the conceptual principles and tries to give an overview of the topic. The other part goes into detail on implementing this architecture with Angular.
What is a component?
Let’s start by really taking a look at what defines a component. Taking code-of-some-kind and creating reusable components of it is done for as long as there are frameworks. Starting with jQuery and such, continuing with Single Page Application Frameworks such as Angular to the latest pinnacle framework less web-native Web-Components. As seen by the recently emerging Web Components, a Component can be very atomic in its definition, scope, and usage. Therefore, let’s define components as encapsulated and reusable elements, with a clear use-case and nice API. Of course, a component should also be testable.
Now that we know what a component should be, we also must look at principles that make it a good component compared to others.
Important principles for good components
Why do developers care to write components? Why not just put everything in one place and be done with it? Because we must make sure that it outlives the development cost, it must be maintainable, testable, and extensible.
One core principle that enables this is DRY „Don’t repeat yourself“, reuse code if you can, or even better scope classes, services, and components to their needs. Those scopes should create a component with straightforward usage and responsibility.
That is also the second principle: Extract and specify responsibilities and scopes. Try to write components that solve one problem, not many.
The third principle is „Open/Close“ – „Open for extension and closed for modification“. Let’s say you got a dropdown component, extensible means that you can provide another component as a toggle button or even use that dropdown as display content for a combo box. A modification would mean that you can use the simple dropdown as the combo-box itself, containing all components itself (the activator Button, the content items, and the dropdown pane), switching between use cases by toggling variables. That would also violate the second rule: solving problems out of scope.
The fourth, final, and maybe most important concept is „Inversion of control“. It is quite a large topic and requires an article by itself, but we try to find a brief explanation for this article. Components should be „servants“ to the components that come before or to the outer context. For example, the outer component or context can create that relationship if it provides a service and overrides the standard provided service. With that, the outer component can control what the inner does without explicitly interacting with it. Other examples are Angular’s content projection with
ng-content component or HTML template slots. The outer component defines what the inner component displays – I could easily find hundreds of more examples in serval languages and Frameworks. It is a vast topic.
However, Inversion of Control plays nicely with the other principles. It enables us to keep the scopes clean.
Enough of those boring concepts and definitions; let’s get to the core. There are two different component types in this architecture, the presentational components displaying stuff and the smart components handling data, application logic, and more. What makes a component an excellent presentational component?
What is a presentational component?
Presentational components are standard UI components. Their purpose is to display information and interact with the user. They should have few to no dependencies on services or other contexts. Of course, one component can be split-down further to increase maintainability and the ease of testing. Presentational components should only use a local state and logic where necessary, like controlling a checkbox or managing the opening and closing of a dropdown. They are somewhat trimmed down to the core of their functionality, displaying data and interacting with a user. However, they can receive data from other components and output data to other components, e.g., surrounding smart components or other UI components.
Let’s head over to smart components, providing global data, logic, and connecting to other contexts.
What is a Smart Component?
While presentational components handle the UI and user interaction, smart components care about the app state and general logic. They connect different parts and combine multiple presentational components into a bigger picture. In general smart components should only contain presentational components, as long as they represent a single „view“ in your application. They include services and data sources of all kinds, connecting different app parts and app states.
That’s all; it isn’t that much, right? It is a pretty broad and loose topic. So let’s recap the most important aspects.
- Try to be use-case Agnostic
- Write small components
- Use clear naming and scopes
- Define clean responsibility and scoped components
- Move shared logic between components into services
- Separate UI and Logic
As with all architectures, this one is not free of flaws either. As we are optimizing, cutting components, and scoping contexts, we tend to optimize the application eagerly. We tend to build features that are not necessary. This comes in naturally because this architecture is made to build features and to create reusable, extendable components. The biggest drawback may be the upfront development time. It takes time to discuss the feature and how to create components properly. At best, it is done in pair with another person. Using this pattern becomes easier and speeds up over time and experience – discussing it with another person will always help.
Knowing what to be aware of, we can take a sophisticated look at the advantages.
Advantages and profits
Having small components with clear scoped responsibilities brings multiple advantages. For once, the onboarding process can become more accessible. It is now possible to create smaller tasks with a clear agenda. That way, the things a person has to know become fewer. A new colleague could start exploring the application on its own. The same reason enables us to write good tests with ease. There are less side effects to be aware of and fewer dependencies to mock. A clean API also provides the necessary parts to make the next development steps easier. We trade some up-front development time for faster, safer, and less complex development in a later state. We know that with time and effort, an app will become more complex and hard to develop further. With this architectural approach, we can reduce that complexity in the future. That way, we can easily add new features later and react fast to changing requirements.