This post is part of the ongoing Angular Architecture series, where we cover common design problems and solutions at the level of the View Layer and the Service layer. Here is the full series:
- View Layer Architecture - Smart Components vs Presentational Components
- View Layer Architecture - Container vs Presentational Components Common Pitfalls
- Service Layer Architecture - How to build Angular apps using Observable Data Services
- Service Layer Architecture - Redux and Ngrx Store - When to Use a Store And Why?
- Service Layer Architecture - Ngrx Store - An Architecture Guide
Introduction to View Layer Architecture
When building an Angular application, one the most frequent questions that we are faced with right at the beginning is: how do we structure our application?
The immediate answer that we might find is: we just split everything into components! But we then quickly find out that there is more to it than that:
- what types of components are there?
- how should components interact?
- should I inject services into any component?
- how do I make my components reusable across views?
We will try to answer these and other questions by splitting components essentially into two types of specialized components (but there is more to it):
- Smart Components: also know sometimes as application-level components, or container components
- Presentation Components: also known sometimes as pure components or dumb components
Let's find out the differences between these two types of components, and when should we use each and why!
If you like to learn via video, here is a refactoring of a component into two components of each type, the example in the video is the one we will describe below (Subscribe in YouTube to get similar videos):
Splitting an application into different types of components
In order to understand the difference between the two types of components, let's start with a simple application, where the separation is not yet present.
We started building the Home screen of an application and we have added multiple features to a single template:
Understanding the Problem
Although this Homepage component is still very simple, it's already starting to have a significant size. For example, we have implemented a table containing a list of lessons.
But there will likely be other parts of the application where this functionality is also needed, for example let's say that we have another screen which presents the table of contents of a given course.
In that screen we also want to display a list of lessons, but only the lessons that belong to that course. In that case, we would need something very similar to what we have implemented in the Home screen.
And we shouldn't just copy-paste this across components, we should create a reusable component, right?
Let's create a Presentation Component
What we will want to do in this situation is to extract the table part of the screen into a separate component, let's call it the LessonsListComponent
:
Now let's take a closer look at this component: it does not have the lessons service injected into it via its constructor. Instead, it receives the lessons in an input property via @Input
.
This means that the component itself does not know where the lessons come from:
- the lessons might be a list of all lessons available
- or the lessons might be a list of all the lessons of a given course
- or even the lessons might be a page in any given list of a search
We could reuse this component in all of these scenarios, because the lessons list component does not know where the data comes from. The responsibility of the component is purely to present the data to the user and not to fetch it from a particular location.
This is why we usually call to this type of component a Presentation Component. But what happened to the Home Component?
Let's create a Smart Component
If we go back to the Home component, this is what it will look like after refactoring:
As we can see, we have replaced the list part of the Home screen with our new reusable lessons list component. The home component still knows how to retrieve the lessons list from a service, and what type of list this is (if these lessons are the lessons of a course, etc.).
But the Home component does not know how to present the lessons to the user.
What type of component is the Home component?
Let's give a name to this type of components similar to the home component, which is an application-specific component: let's call this a Smart Component.
This type of component is inherently tied to the application itself, so as we can see it receives in the constructor some dependencies that are application-specific, like the LessonsService
.
It would be very hard to use this component in another application.
The top level component of our view will likely always be a smart component. Even if we transfer the loading of the data to a router data resolver, the component would still at least have to have the ActivatedRoute
service injected into it.
So we want to implement the top-level smart component by composing it internally using a set of presentation components. And it's as simple as that. Or is it?
Typical interaction between smart components and presentation components
The example that we see here is very frequent, we have the smart component inject the data to the presentation component via @Input
, and receive any actions that the presentation component might trigger via @Output
.
In this case, we are using the custom lesson
event to indicate that we have selected a given lesson in the list.
Using @Output
the presentation component remains isolated from the smart component via a clearly defined interface:
- the lessons list presentation component only knows that it emitted an event, but does not know what are the receivers of the event or what will the receivers do in response to the event
- the home screen smart component subscribes to the
lesson
custom event and reacts to the event, but it does not know what triggered the event. Did the user double click on the lessons list or did the user click a view button? This is transparent to the smart component.
So this is all clear and simple, what could go wrong here?
A clear way to split smart vs presentation components?
With this, we might at this point conclude that building our application is as simple as making all top level components smart components, and build them by using a local tree of presentation components.
But the thing is, it's sometimes not that simple because custom events like
lesson
don't bubble up. So if for example you have a deep tree of components and you want a component multiple levels above to know about the event, the event will not bubble.
What problems does it cause the fact that custom events don't bubble?
Let's say that instead of only one level of nesting between the lesson list and the home component, we have several levels: the lessons list is inside a collapsible panel which is inside a tab panel.
The lesson list still wants to notify the home component that a lesson was selected via the lesson
event. But the two components in the middle TabPanel
and CollapsiblePanel
are non application-specifc Presentation Components.
imagine they were components of the Angular Material library!
These Presentation-only components do not know about the lesson
event, so they cannot bubble it up. So how do we implement this, and also why can't custom events simply bubble up?
Why don't custom event bubble up, like it's the case with DOM events like click?
This is not an accident, it's by design and probably to avoid event soup scenarios that the use of solutions similar to a service bus like in AngularJs $scope.$emit()
and $scope.$broadcast()
tend to accidentally create.
These type of mechanisms tend to end up creating tight dependencies between different places of the application that should not be aware of each other, also events end up being triggered multiple times or in a sequence that is not apparent while just looking at one file.
So the custom event of a presentation component will only be visible by its parent component and not further up the tree.
If by some reason we really need the bubbling behavior we can still implement it with plain Javascript using element.dispatchEvent()
. But most of the times this is not what we want to implement.
So how do we solve the situation of the lessons list inside the collapsible panel inside the tab panel scenario?
We should still likely create a presentation component for the lessons list. The functionality to present the lessons can be isolated, so the version of LessonsListComponent
still applies, it's just something that will be used everywhere in the application. But how can this list notify the home component?
For that there are several solutions. One solution that we should look into especially if building a large scale application is to look into solutions like ngrx/store
.
But even with a store solution, we might not want to inject the store in the presentation component. Because the result of selecting a lesson might not be always to dispatch an event to the store.
To keep the example simple, let's start by creating a specialized store-like service to solve only this lesson selection problem:
As we can see the LessonSelectedService
exposes an observable selected$
which will emit a value each time that a new lesson is selected.
Notice that we created internally in the service a subject, but we have not exposed it to the outside. This is because a subject is essentially an event bus so we want to keep the control of who can emit events inside the service.
If we expose the subject we would give any other part of the application the ability to emit an event on behalf of the service, and that is to be avoided.
So how can this service be used, because we can't inject it in the LessonsListComponent
, right? We will get to that part, right now let's see first how we can use this new service in the Home component.
Using the new service in Home Component
What the Home component will do is, it will have the new component injected in its constructor:
As we can see, we have subscribed to the selected$
Observable, which emits new lessons and we trigger the specific logic of the Component to process the selection.
But notice that the Home component does not know about the lessons list, it just knows that some other part of the application triggered a lesson selection. The two parts of the application are still isolated:
- the emitter of the selection does not know about the Home component
- the home component does not know about the lesson
- both actors only know about the
LessonSelectedService
So we have fixed the problem, right? Not yet, because we still don't want to inject the new service in the LessonListComponent
, this would make it a smart component and we want to keep it a Presentation component. So how to solve this?
How to keep the LessonsListComponent a Presentation Component?
Actually, one way to solve the problem it to make it a Smart Component ;-) We might get to the conclusion that anywhere on the application that this table exists, we always want to trigger a call to the LessonSelectedService
.
This would make the lessons list component an application specific component which it probably already was anyway. We would probably not ship this component and use it in several applications for example.
So this would solve the problem, and this means that a top level application component like the Home component might be composed of a tree of components that are not only Presentation components.
A Smart Component is not only a top-level component
A Smart Component does not have to be a top level router component only. We can see that there could be other components further down the tree that also get injected a service like LessonSelectedService
, and don't necessarily get their data only from @Input()
.
Another solution for keeping the LessonsListComponent a Presentation Component
Another way of solving the problem is to keep the lessons list component like it is and use it wherever needed. But in this case we can wrap it in a smart component, which gets injected the LessonSelectedService
:
In this we have created a wrapper smart component, and called it CustomLessonsListComponent
. In this case, we wrapped our own presentation component, but we could be wrapping a component that came from a third party library as well.
Imagine a MyCustomCountrySelectDropdown
, that wraps a generic dropdown and injects it with the data coming from a concrete service.
How to decide what components to build?
It's not always apparent when starting to build our application what will be a component, and what will be a smart vs a presentation component.
So how to split the application up in many components? Should the header of a page be a component, even if it's used only once?
Organization and readability are alone reasons to create a component even if it's only used in one place. Separating things in smaller files helps to keep to code base maintainable and with the Angular CLI there is no overhead in creating a new component: with a single command, we have a working component where to paste the header in seconds.
How to approach component design
One way to approach this is to avoid defining from the start what will be a component and of what type: we can start by building the top-level component using only plain HTML and third party components.
And only when the template starts getting bigger we start splitting it up into components. If something is used in several parts of the screen and always triggers a given action like calling a store dispatch, we might consider refactoring to a smaller smart component.
If later we realize that we need to present the same data like the smart component that we just created, we can extract the presentation part from it into a presentation component.
The best approach to get a great set of well-designed components is by continuous refactoring, which can be done without burden by using the Angular CLI.
Summary
Angular components can be roughly categorized into two different types:
- Presentational components: these components are responsible only for displaying data, without knowing where the data comes from. They receive the data via an
@Input()
, and return modified data throughOutput()
events - Smart components: these components are responsible for interacting with the service layer and retrieving the data, which then they pass to presentational components
While building an application, we can look out for opportunities to extract the pure presentation logic into Presentation Components: these only use @Input
and Output
, and are useful in case we need to isolate the presentation logic and reuse it.
Communication between smart components at different levels in the component tree or even siblings can be done using a shared service or a store if we want to keep the two components decoupled and unaware of each other.
But we might also want to inject components completely into each other and create a tight coupling, sometimes that is the best solution. In that case injecting the components into each other via for example @ViewChild
might be the best approach.
Smart vs Presentation Components is a useful distinction
In general the distinction between smart vs presentation components is very useful to keep in mind, but it might not be applied to all the components of the application.
We could have a small component that is both aware of a service and presents some data deeper in the tree, like a lessons list that calls a store upon lesson selection.
Splitting this component up further into a smart and a presentation component might not always be necessary.
Smart vs Presentation Components is more of a mindset to adopt where we ask ourselves:
- would this presentation logic be useful elsewhere in the application?
- would it be useful to split things up further?
- are we creating unintended tight couplings in the application?
We don't necessarily need to extract all the rendering logic of every component we build into a separate presentation component. It's more about building the components that make the most sense to our application at any given time, and refactor if needed in a continuous iterative process made simple by the CLI.
I hope that you enjoyed the post and that it helped getting started with view layer design. Make sure to see the other posts on the Architecture series, linked above!
I invite you to subscribe to our newsletter to get notified when more posts like this come out:
And if you are looking to learn more about Angular application design patterns, we recommend the Reactive Angular Course, where we cover lots of commonly used reactive design patterns.
If you are just getting started learning Angular, have a look at the Angular for Beginners Course:
Other posts on Angular
If you enjoyed this post, have also a look also at other popular posts that you might find interesting:
- Angular Router - How To Build a Navigation Menu with Bootstrap 4 and Nested Routes
- Angular Router - Extended Guided Tour, Avoid Common Pitfalls
- Angular Components - The Fundamentals
- How to build Angular apps using Observable Data Services - Pitfalls to avoid
- Introduction to Angular Forms - Template Driven vs Model Driven
- Angular ngFor - Learn all Features including trackBy, why is it not only for Arrays ?
- Angular Universal In Practice - How to build SEO Friendly Single Page Apps with Angular
- How does Angular Change Detection Really Work ?
- Typescript 2 Type Definitions Crash Course - Types and Npm, how are they linked ? @types, Compiler Opt-In Types: When To Use Each and Why ?