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
Let's then talk a bit about Angular Component Architecture: We are going to present a very common component design and a potential design issue that you might run into while applying it.
A Common Component Design (And a potential issue with it)
A really important aspect of Angular application development is application component design: How to combine different types of components together, when to use a component vs a directive, how and when to extract functionality from components into directives, etc.
Angular Components provide a lot of functionality that can be used and combined in many different ways. This allows us to adopt a large variety of application designs depending on the situation.
In this post, we are going to talk about one particular type of design that we often hear about.
Container Components vs Presentational Components
The design scenario that we will be talking about is the separation of components between Container Components vs Presentational Components.
This is a popular design that is now being used more and more in the Angular ecosystem since now Angular supports the Component model. The design is presented in this blog post by Dan Abramov (@dan_abramov):
The post is for React but the notions apply also to any ecosystem that allows a component based design model, like Angular.
An example of the Container vs Presentational Design
So let's give a quick example of this design, please bear in mind that different terms are used to name the different types of components.
The core idea of the design is that there are different types of components. Using the same terminology as in the blog post above we have:
- Container Components: These components know how to retrieve data from the service layer. Note that the top-level component of a route is usually a Container Component, and that is why this type of components where originally named like that
- Presentational Components - these components simply take data as input and know how to display it on the screen. They also can emit custom events
Let's give a simple example of this design, that will actually already contain a potential design issue. To keep it more fun I suggest the following: try to spot the design issue as I present the example, and we will go over the issue later in this post.
If you have already tried to use this design, most likely you have come across this problem.
A Top-Level Component Written in Reactive Style
So let's get started with the top-level component of our route. Let's have a look at a simple route top-level component written in reactive style:
This is a simple component that presents the details of a course: in contains a header that contains a summary of the course (plus a newsletter box) and a list of lessons.
So let's break it down what we have in this top-level component and how it's currently designed:
- the component has router dependencies injected, but also some application-specific services
- the component does not have any variables that keep direct references to data, like lessons or courses
- instead, the component declares a few observables on
ngOnInit, that are derived from other observables obtained from the service layer
Top-Level Component Design Overview
This top-level component will define how to get the data from the service layer, based on a routing identifier parameter.
This is a typical top-level component in a reactive style application that does not use router data pre-fetching (more on this later). This component will be displayed without any data initially, and it will do one or more calls to the service layer to fetch the data.
Notice that the component is just defining a set of Observables, but no subscriptions are done in the component class: so how is the data displayed then?
The Template of The Top-Level Component
So let's now have a look at the template for this component, and see how these observables are being used:
As we can see, we are taking the observables and we are subscribing to them via the async pipe. The data then gets applied to a tree of local components that exist under the top-level component of this route:
- multiple types of data, including the user, the lessons, and the courses are being passed to the
course-detail-headercomponent, as well as to a list of lessons.
- These components are responsible for presenting the data retrieved by the top level component
A note on multiple subscriptions
One important thing: the
lessons$ observable is being subscribed two times. In this case, it would not pose a problem because the observable coming from the service layer is designed to prevent multiple requests to the backend, for example using
Note that this is just one possible solution for ensuring that multiple subscriptions are not a problem. So let's have a look now at one of these local components that are being used in the top-level component template. We will see that they have a very different design.
Looking into the design of a Presentational Component
So the top-level component is a container component, but what about the other components that used in the template?
Presentational components will take care of taking input data and presenting it to the user. So for example, the
course-detail-header is a presentational component. Let's have a look at what this component looks like:
Reminder: try to spot the issue with this design
As we can see, the component takes as input some data that then gets rendered to the screen. This component does not have dependencies with the service layer of the application, instead, it receives its data via inputs.
It also emits outputs, such as for example the
subscribe output event. But where is this event coming from? We can see that is being triggered in response to an event with the same name coming from the
And what does the newsletter component look like, how is it designed? Let's have a look.
A Presentational Component one level deeper the Component Tree
The newsletter component is also a presentational component, because it takes an input, displays a subscription form and emits an event upon subscription:
So this is the design we currently have for the newsletter component, so let's review it in more detail to see what the problem could be.
A Potential issue with this design
You might have noticed a couple of things in the newsletter component that are similar to the
- the input property first name
- the output event
Both of these elements are repeated in the two components. So it looks like we are doing a couple of things in this design that would likely not scale well in a larger component tree, because there is a lot of repetition involved.
Let's go over these two issues and see how could we potentially design this otherwise.
Design Issue 1 - Extraneous Properties in Intermediate Components
It looks like we are passing inputs like
firstName over the local component tree, in order for leaf components like the newsletter component to use them. But the intermediate components themselves are not using the inputs, they are just passing them to their child components.
Usually, the local component tree is much larger than this example, so this issue would potentially result in a lot of repetition of inputs.
Also and more importantly: if we are using third-party widget libraries and we use some of those components as intermediate components, we might have trouble passing all the necessary data through the component tree, depending on how the libraries are designed.
There is also another similar issue, which is linked to the outputs.
Design Issue 2 - Custom Event Bubbling Over the Local Component Tree
As we can see, the
subscribe event is being also repeated at multiple levels of the component tree, this is because by design custom events do not bubble.
So also here we have a code repetition problem that won't scale well for a larger example, and won't work with third-party libraries - we wouldn't be able to apply this technique in that case.
Also, the logic for subscribing to the newsletter (the call to
newsletterService) is on the top-level route component, and not on the newsletter component.
This is because only the top-level component has access to the service layer, but this ends up causing that potentially a lot of logic is kept on that component because of that.
So how can we solve these issues in Angular? Let's have a look at a possible solution.
Preventing Custom Event Bubbling
If we find ourselves in a situation where we are manually bubbling events up the component tree, that might work well for certain simpler situations. But if the event bubbling / extraneous properties start to become hard to maintain, here is an alternative.
We are going to present the alternative via a step by step refactoring. Let's start our refactoring from our top-level component again, and see how the new solution avoids the issues that we have identified.
If you would like to have a look at a video version of refactoring ver similar to this one, have a look at this video:
The refactored top-level component
Let's change the top-level component so that it no longer passes as much data or receives as many events from the local component tree. Let's also remove the newsletter subscription logic.
The new version of the top level component now has a lot less code than before:
So this looks like a good start. And what about the top-level component template ? The new template is mostly identical after the refactoring, except for the
This is looking better than the version we had before: we don't see the passing of
firstName or the bubbling of the
subscribe event anymore.
So what does the
course-detail-header intermediate component now looks like after the refactoring?
The refactored intermediate component
We can see here that the new version of the
course-detail-header component is now much simpler after the refactoring:
This new version of the component still contains the newsletter, but it no longer is bubbling events and passing data that is not needed by the component itself.
So again this looks a lot better than the version we presented initially. But where is now the functionality for subscribing to the newsletter?
Let's now have a look at the final component in this refactoring: the leaf component.
The refactored leaf component
As we can see, the newsletter leaf component is now designed in a completely different way:
So what is now the biggest design difference in this new version of the newsletter component?
The biggest difference is actually that this new version looks a lot like a Container Component!
So as we can see, sometimes the best solution is to inject services deep into the component tree. This really simplified all the multiple components involved in this example.
But this implementation of the Leaf component could be further improved, so let's review in further detail this design to see how.
Reviewing the new component design solution
The new design for this particular tree of components seems to be more maintainable. There is no longer bubbling of custom events or the passing of extraneous input properties through the component tree.
The newsletter component is now aware of the service layer and is currently getting all its data from it. It has a reference to the newsletter service, so it can call it directly. Note that this component could still receive inputs if needed, more on this later.
Leveraging Angular features to get a simpler design
We can see that in this new version of the component tree, we are now leveraging the Angular Dependency Injection system for injecting services deep in the local component tree.
This allows deeply nested components like the newsletter component to receive data from the service layer directly, instead of having to receive it via inputs.
This makes both the top-level component and the intermediate components simpler and avoids code repetition. It also allows for logic for interaction with the service layer to be put deeply into the component tree, if that is where it makes the most sense to have it.
One problem with this current implementation of the newsletter component
There is just one problem with this new version of the newsletter component: unlike the previous version which was a presentational component:
this new version will not work with OnPush change detection!
Making the newsletter component compatible with OnPush
You might have noticed before that sometimes when we switch a component to use OnPush change detection, things stop working - even if we are not doing local mutation of data at the level of the component.
And one example of that would be the current version of the newsletter component, which indeed would not reflect new versions of the first name on the template.
But here is a version of the component that is compatible with
OnPush as well:
So what is the difference with this new implementation? In this version of the component, we have defined a first name observable, and we have consumed it in the template using the async pipe.
The use of the async pipe will ensure that the component will be re-rendered when a new version of the first name is emitted (for example when the user logs in), even if the component has no inputs - because the async pipe will detect that a new value was emitted by the observable and so it will then mark the component for re-rendering.
So as we can see there are many possible component designs, depending on the situation. Using the Angular Dependency Injection system really makes it simple to inject services deep in the component tree if we need to.
So we don't necessarily have to pass data and events through several levels of the component tree, as that might cause maintainability problems due to code repetition (among other problems).
But then why does this end up happening so often while trying to apply the Container + Presentational design?
A possible explanation for the custom event bubbling problem
There is one likely main reason why this design might end up being used: in software design the names we give things can have a lot of impact.
And the name
Container Components makes us think that only the top-level component of the route should have that type of design and that all other components used by it should then be presentational, which is not the case.
Container does not make us think of a leaf component like the newsletter component.
So to avoid this issue, here is a suggestion: If we need to give a name to components that are aware of the service layer, and having a name helps in application design discussions, we could call them for example something like Smart Components instead, and reserve the term Container to the top-level component of the route only.
In practice its actually much more practical to mix and match the multiple types of component design as we need, and use different types of components at different levels of the tree as necessary - mixing the different features as much as we need.
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:
Video Lessons Available on YouTube
Have a look at the Angular University Youtube channel, we publish about 25% to a third of our video tutorials there, new videos are published all the time.
Subscribe to get new video tutorials:
Other posts on Angular
If you enjoyed this post, have also a look also at other popular posts that you might find interesting:
- Angular Smart Components vs Presentation Components: What's the Difference, When to Use Each and Why?
- 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 ?