This post continues our Angular Architecture series, which explores recurring design challenges and their solutions at both the View Layer and the Service layer. The complete series is listed below:
- 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
Now, let's focus on Angular Component Architecture. We'll examine a common component design and a subtle issue you might encounter when applying it.
A Common Component Design (And a Potential Issue)
A crucial part of Angular development is component design—deciding how to assemble different component types, choosing between components and directives, and figuring out when to extract logic from components into directives.
Angular Components offer a rich set of features that can be mixed and matched in many ways, allowing for a wide range of application architectures depending on the specific context.
In this post, we'll delve into one such widely-used design pattern.
Container Components vs Presentational Components
The design we'll discuss involves dividing components into two categories: Container Components and Presentational Components.
This pattern has gained popularity in the Angular ecosystem because Angular natively supports a component-based model. The concept was originally introduced in this blog post by Dan Abramov (@dan_abramov):
Presentational and Container Components
While the article is written for React, the principles apply equally to any ecosystem that supports a component-based design, including Angular.
An Example of the Container vs Presentational Design
Let's illustrate this design with a quick example. Please note that the terminology for these component types can vary.
The fundamental idea is that components serve different roles. Using the terminology from the linked article, we have:
- Container Components: These components are responsible for fetching data from the service layer. The top-level component of a route is typically a Container Component, and this is the origin of their name.
- Presentational Components: These components accept data as input and are concerned only with displaying it on the screen. They can also emit custom events to communicate with their parent components.
Let's look at a simple example of this design. It will already contain a potential design issue. To make it more interactive, I suggest trying to spot the issue as I walk through the example. We'll discuss it in detail later in the post. If you've used this design before, you've likely run into this exact problem.
A Top-Level Component Written in Reactive Style
Let's begin with the top-level component of a route. Here is a simple example written in a reactive style:
This component displays the details of a course. It includes a header with a course summary (along with a newsletter sign-up box) and a list of lessons.
Let's break down what we have in this top-level component and how it's structured:
- The component injects dependencies for routing, as well as some application-specific services.
- The component does not hold direct references to data in properties, such as lessons or courses.
- Instead, it defines a set of Observables in
ngOnInit, which are derived from other Observables provided by the service layer.
Top-Level Component Design Overview
This top-level component is responsible for determining how to retrieve data from the service layer, based on a routing identifier parameter.
This is a standard pattern for a top-level component in a reactive application that does not use route pre-fetching (more on this later). The component is initially displayed without any data and makes one or more calls to the service layer to load the necessary information.
Notice that the component only defines a set of Observables; there are no subscriptions within the component class itself. So, how does the data get displayed?
The Template of The Top-Level Component
Let's now examine the template for this component to see how these Observables are used:
As you can see, we subscribe to the Observables in the template using the async pipe. The resulting data is then passed down to a tree of local components under the top-level component of the route:
- Various types of data, including the user, lessons, and courses, are passed to the
course-detail-headercomponent and a list of lessons. - These local components are responsible for presenting the data fetched by the top-level component.
A Note on Multiple Subscriptions
One important detail: the lessons$ Observable is subscribed to twice in the template. In this instance, it is not a problem because the Observable from the service layer is designed to prevent multiple backend requests, for example by using publishLast().refCount().
Keep in mind that this is just one approach to handling multiple subscriptions. Now, let's look at one of the local components used in the top-level component's template, and we'll see it has a very different design.
Looking into the Design of a Presentational Component
The top-level component is a container. But what about the other components it uses in its template?
Presentational components are designed to take input data and display it to the user. For instance, the course-detail-header is a presentational component. Let's see what it looks like:
Reminder: try to spot the issue with this design
As we can see, this component takes data as input and renders it on the screen. It has no dependencies on the application's service layer; it receives all its data via input properties.
It also emits output events, like the subscribe event. But where does this event originate? It's triggered in response to an event with the same name coming from its child, the newsletter component.
So what does the newsletter component look like? Let's examine its design.
A Presentational Component One Level Deeper in the Component Tree
The newsletter component is also presentational because it takes an input, displays a subscription form, and emits an event upon subscription:
This is the current design for the newsletter component. Let's review it in detail to understand what the problem might be.
A Potential Issue with this Design
You may have noticed a couple of things in the newsletter component that are shared with the course-detail-header component:
- The input property
firstName - The output event
subscribe
Both of these elements are duplicated across the two components. This design appears to have repetition that would not scale well with a larger component tree.
Let's address these two issues and explore how we might redesign this architecture.
Design Issue 1 - Extraneous Properties in Intermediate Components
We are passing inputs like firstName down the local component tree so that leaf components like the newsletter component can consume them. However, the intermediate components themselves do not use these inputs; they only pass them along to their children.
In a real application, the component tree is typically much deeper than in this example, meaning this issue can lead to a significant amount of input property repetition.
More importantly, if you are using third-party widget libraries and use those components as intermediate components, passing all the required data down the tree can be problematic, depending on how those libraries are structured.
There is also another, similarly structured issue related to outputs.
Design Issue 2 - Custom Event Bubbling Over the Local Component Tree
As shown, the subscribe event is also repeated at several levels of the component tree because custom events do not bubble up by design.
This again leads to code repetition that will not scale for larger examples and will be impossible to manage with third-party libraries, as we cannot apply this technique in that scenario.
Additionally, the logic for subscribing to the newsletter (the call to newsletterService) is located in the top-level route component, not in the newsletter component itself.
This is because only the top-level component has access to the service layer, which can cause a lot of logic to accumulate there.
How can we solve these issues in Angular? Let's look at a possible solution.
Preventing Custom Event Bubbling
Manually bubbling events up the component tree might work for simpler scenarios. However, if the event bubbling and extraneous properties become difficult to maintain, here is an alternative approach.
We'll present this alternative through a step-by-step refactoring. We'll start with the top-level component again and see how the new solution avoids the identified issues.
If you'd like to see a video version of a similar refactoring, check out this video:
The Refactored Top-Level Component
Let's modify the top-level component so it no longer passes as much data or accepts as many events from the local component tree. We'll also remove the newsletter subscription logic.
The new version of the top-level component has considerably less code:
This looks like a good start. And what about the template for the top-level component? The new template is mostly the same as before, except for the course-detail-header component:
This looks better than our previous version: we no longer see the passing of firstName or the bubbling of the subscribe event.
So, what does the course-detail-header intermediate component look like after this refactoring?
The Refactored Intermediate Component
The new version of the course-detail-header component is much simpler now:
This version still contains the newsletter, but it no longer bubbles events or passes data that it doesn't need itself.
Again, this is a lot better than the initial version. But where does the newsletter subscription logic live now?
Let's look at the final component in this refactoring: the leaf component.
The Refactored Leaf Component
The newsletter leaf component is now designed quite differently:
So, what is the most significant design difference in this new version of the newsletter component?
The biggest change is that this new version closely resembles a Container Component!
As we can see, sometimes the best solution is to inject services deep into the component tree. This simplified all the components involved in our example.
However, this implementation of the leaf component could be further improved. Let's review this design in more detail to see how.
Reviewing the New Component Design Solution
The new design for this component tree appears to be more maintainable. We no longer have the bubbling of custom events or the passing of extraneous input properties.
The newsletter component is now aware of the service layer and retrieves its data from it directly. It holds a reference to the newsletter service, so it can call it as needed. Note that this component could still accept inputs if required; more on that later.
Leveraging Angular Features to Get a Simpler Design
In this new version of the component tree, we are leveraging Angular's Dependency Injection system to inject services deep into the local component tree.
This allows deeply nested components, such as the newsletter component, to get data from the service layer directly, instead of receiving it through a chain of inputs.
This makes both the top-level and intermediate components simpler and avoids code repetition. It also permits interaction logic with the service layer to reside deep within the component tree, if that is where it makes the most sense.
One Problem with the Current Newsletter Component Implementation
There is one issue with this new version of the newsletter component: unlike the previous version, which was presentational,
this new version will not work with OnPush change detection!
Making the Newsletter Component Compatible with OnPush
You might have noticed that sometimes when you switch a component to use OnPush change detection, things stop working—even when you are not locally mutating data within the component.
An example of this would be the current version of the newsletter component, which indeed would not reflect new versions of the firstName in its template.
Here is a version of the component that is compatible with OnPush:
What is the difference in this new implementation? We have defined a firstName Observable and consumed it in the template using the async pipe.
Using the async pipe ensures the component will be re-rendered when a new value for firstName is emitted (for example, when the user logs in), even if the component has no inputs. This works because the async pipe detects the new value from the Observable and marks the component for re-rendering.
Conclusions
As we can see, there are many possible component designs depending on the situation. Using Angular's Dependency Injection system makes it straightforward to inject services deep into the component tree when necessary.
We don't necessarily have to pass data and events through multiple levels of the component tree, as doing so can cause maintainability problems such as code repetition.
But why does this pattern end up being applied so often when trying to use the Container + Presentational design?
A Possible Explanation for the Custom Event Bubbling Problem
There is one likely main reason why this design gets misapplied: the names we give things in software design can significantly influence our choices.
The term Container Components suggests that only the top-level component of a route should have that design, leading us to believe all other components should be presentational. This, however, is not the case.
The word Container does not make us think of a leaf component like the newsletter component.
To avoid this issue, here is a suggestion: if we need a term for components that interact with the service layer, and a name helps in design discussions, we could call them Smart Components instead. We can then reserve the term Container for the top-level component of a route only.
In practice, it is much more practical to mix and match various component designs as needed, using different component types at different levels of the tree and combining features as necessary.
I hope you enjoyed this post and found it helpful for getting started with view layer design. We invite you to subscribe to our newsletter to get notified when more posts like this are published:
If you'd like to learn more about Angular application design patterns, we recommend the Reactive Angular Course, where we teach many commonly used reactive design patterns for building Angular applications.
If you are just getting started with Angular, check out the Angular for Beginners Course:
Other Posts on Angular
If you enjoyed this post, you might also find these other popular articles 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 ?
