View Layer Architecture: An Overview
This post continues our Angular Architecture series, focusing on common design challenges at the View Layer and Service layer levels. The complete series covers:
- 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
Getting Started with View Layer Architecture
A common question that arises early in any Angular project is: how exactly should we organize our application?
The obvious answer often seems to be: break everything into components! However, we soon realize that the situation is more nuanced:
- what categories of components exist?
- how do components communicate with each other?
- is it acceptable to inject services into any component?
- how can we ensure components work across different views?
We aim to address these questions by categorizing components into two primary types (though there's more to explore):
- Smart Components: sometimes referred to as application-level components or container components
- Presentation Components: also known as pure components or dumb components
Let's explore the distinctions between these component types, and understand when each is appropriate and why!
Prefer video content? Below is a refactoring of a single component into both types; the example mirrors what we'll describe next (Subscribe on YouTube for similar content):
Dividing an application into component types
To see the distinction between these component types, consider a basic application that hasn't yet implemented this separation.
We've begun creating the Home screen, incorporating several features into a single template:
Identifying the Issue
Even at this early stage, the Homepage component is growing noticeably large. For instance, we've built a table that displays a list of lessons.
Other parts of the application will likely require similar functionality; imagine another screen that shows a table of contents for a course.
On that screen, we'd also want to display lessons, but only those specific to that course. This is very similar to what we've built for the Home screen.
Rather than duplicating code, we should extract this into a reusable component, correct?
Building a Presentation Component
In this scenario, the logical step is to isolate the table into its own component — let's call it LessonsListComponent:
Examine this component closely: the lessons service is not injected via its constructor. Instead, lessons are passed in as an input property using @Input.
This means the component is unaware of the data source:
- the lessons could be the complete set available
- they could be lessons belonging to a specific course
- they might even represent a page of search results
This component can be used in any of these situations precisely because it doesn't know where the data comes from. Its sole purpose is to render the data, not to retrieve it from anywhere.
That is why we call this a Presentation Component. But what becomes of the Home Component?
Creating a Smart Component
After refactoring, the Home component looks like much less:
The list portion of the Home screen has been swapped for our new reusable lessons list. The home component still handles retrieving the lessons from a service and understands the context (e.g., whether these are lessons for a particular course).
On the other hand, the Home component has no idea how to display the lessons to the user.
What Category Does the Home Component Belong To?
We can label the Home component type as application-specific: let's call it a Smart Component.
Such components are inherently tied to the application. Indeed, its constructor receives application-specific dependencies like LessonsService.
It would be almost impossible to reuse this component in another application.
The root component of a view is almost always a smart component. Even if we use a router data resolver to load data, the component still needs the ActivatedRoute service injected.
So the plan is simple: build the top-level smart component by composing it with a collection of presentation components. And that's all there is to it. Or is there more?
How smart and presentation components usually interact
Here we see a common pattern: the smart component passes data to the presentation component via @Input and receives user actions through @Output.
In this case, a custom lesson event indicates when a user selects a lesson from the list.
By using @Output, the presentation component stays decoupled from the smart component thanks to a clear interface:
- the lessons list component knows it emitted an event but is unaware of who is listening or what they might do in response
- the home screen smart component catches the
lessonevent and handles it, but doesn't know what caused it. Was it double-click or a button click? The smart component doesn't need to know.
Seems straightforward — what could possibly go wrong?
Is There a Clear-Cut Division?
At this point, it's tempting to conclude that building applications is mere child's play: root components are smart components, built with a tree of presentation components.
However, reality is often more complex because custom events like lesson don't bubble up. So, if you have a deeply nested component tree and want an ancestor component higher up to know about an event, the event won't reach it by default.
What's the Consequence of Custom Events Not Bubbling?
Consider this scenario: instead of just one level between the lesson list and the home component, we have multiple layers: the lesson list resides inside a collapsible panel which is inside a tab panel.
The lesson list still needs to tell the home component about a selection using the lesson event. But the intermediate components — TabPanel and CollapsiblePanel — are non-application-specific Presentation Components.
imagine they were from the Angular Material library!
These presentation-only components are not aware of the lesson event, so they can't propagate it upwards. How can we handle this, and why can't custom events bubble up naturally?
Why Don't Custom Events Bubble Like DOM Events (e.g., click)?
This is intentional, by design, likely to prevent the event soup that mechanisms like AngularJs's $scope.$emit() and $scope.$broadcast() tend to create unintentionally.
Such mechanisms often create tight coupling between different parts of an app that don't need to know about each other. They also can cause events to fire multiple times or in sequences that are hard to trace when examining a single file.
Thus, a presentation component's custom event is only visible to its direct parent, not up the entire tree.
If you truly need bubbling behavior, it's possible to implement using plain JavaScript with element.dispatchEvent(). But usually, that's not what we want to do.
How Do We Fix the Lesson List in the Nested Panel Scenario?
We should still create a presentation component for the lessons list, as the logic to render lessons can be isolated. The LessonsListComponent is still valid; it's something reusable across the app. But how does the list notify the home component?
There are a few methods. For large-scale applications, solutions like ngrx/store are worth investigating.
Yet, even with a store, you might not want to inject it into every presentation component. Selection of a lesson doesn't always mean dispatching an event to the store.
For simplicity, let's create a dedicated store-like service to solve just this lesson selection problem:
The LessonSelectedService exposes an observable selected$ that emits a value every time a lesson is selected.
Notice that a subject is created internally, but it's not exposed externally. That's because a subject acts as an event bus, so keeping emission control within the service is crucial.
Exposing the subject would let other parts of the app emit events on the service's behalf, which we should avoid.
So how is this service used, given we can't inject it into LessonsListComponent? We'll cover that in a moment. First, let's see how the Home component uses this service.
Using the New Service in the Home Component
Which is done by injecting the component's constructor:
Here, we subscribe to the selected$ Observable, which emits new lessons, then execute the component's specific logic to handle a selection.
The Home component remains unaware of the lessons list itself; it simply knows that when a lesson gets selected somewhere, it should react. The interaction remains decoupled:
- the sender of the selection is unaware of the Home component
- the home component isn't tied to the lesson's source
- both sides only know about the
LessonSelectedService
The problem appears solved, doesn't it? Not entirely — we still don't want to inject the new service into LessonListComponent, since that would turn it into a smart component. How can we ensure it stays a presentation component?
Keeping LessonsListComponent a Presentation Component
Actually, one possible solution is to just accept it as a smart component ;-) In many cases, wherever the table appears in the application, triggering a call to LessonSelectedService is exactly what's wanted.
In doing so, the lessons list component becomes an application-specific component, which it probably already was. For instance, we likely wouldn't ship this component for use in multiple different applications.
This resolves the issue, meaning that a top-level smart component like Home might actually be made of components that aren't all presentation components.
Why Smart Components Aren't Confined to the Top of the Tree
It's a common misconception that a Smart Component must sit at the root of the routing hierarchy. In practice, components deeper in the tree can also have services injected directly into them, such as LessonSelectedService, and may not rely solely on @Input() for their data.
An Alternative Path to Keeping LessonsListComponent a Pure Presentational Component
We can preserve the `LessonsListComponent` as-is and reuse it anywhere, provided we create a separate smart wrapper around it that handles the service injection, like LessonSelectedService:
To do this, we introduced a wrapper smart component and named it CustomLessonsListComponent. Here we wrapped our own presentational component, but the same logic applies if we want to wrap a third-party component.
Consider a hypothetical MyCustomCountrySelectDropdown that takes a generic dropdown and provides it with data from a specific service.
Deciding Which Components to Create
Determining what will be a component, and whether it should be smart or presentational, isn't immediately clear when a project begins.
How do we break an application into multiple components? Is it worth making the header of a page its own component, even if it appears once?
Beyond reuse, organizational clarity and readability justify creating a component even when used in a single location. Dividing logic into smaller files keeps the codebase maintainable. With Angular CLI, spinning up a new component adds minimal overhead: a single command creates a working shell where new code can be dropped in seconds.
A Practical Approach to Component Design
Instead of trying to define all components and their types upfront, an alternative strategy is to start with the top-level component using plain HTML and third-party elements. As the template expands, we can begin to break it into components. If a section of the screen is reused and always triggers the same action, like dispatching to a store, it may be time to refactor it into a smaller smart component.
Later, if we notice that the same data needs to be displayed elsewhere, we can extract the rendering logic from the smart component into a dedicated presentational component.
The most effective way to reach a solid set of components is through ongoing, incremental refactoring, which becomes simple and routine with the Angular CLI.
Key Takeaways
Angular components can be broadly classified into two types:
- Presentational Components: their only job is to render data, without concern for where it came from. Data arrives through
@Input(), and user actions are emitted viaOutput(). - Smart Components: these handle the interaction with business logic and services, fetching data to pass down to presentational descendants.
During development, we can identify the pure visual logic and extract it into Presentational Components that communicate exclusively through @Input and Output; this isolation simplifies testing and reuse.
Smart components at various tree levels, including siblings, can stay decoupled by communicating via a shared service or a store. Alternatively, strict coupling might be appropriate in some scenarios; in that case, direct injection between components, e.g., via @ViewChild, can be the best path.
The Smart vs. Presentational Distinction Remains Useful
Remembering the two categories is valuable, but no one forces us to strictly classify every single component in an app.
For instance, a compact component deep in a tree might both interact with a service and render data, such as a lessons list that dispatches a store action when selected.
Splitting such a component into a smart/presentational pair is not always essential.
More than a rigid rule, the concept acts as a way of thinking, prompting us to ask ourselves:
- could we reuse this presentation logic somewhere else in the app?
- does the current design benefit from further decomposition?
- are we introducing unnecessary dependencies by over-coupling components?
We aren't required to extract the rendering template from every component into its own pure presentational layer. The goal is to design what serves the application best at that moment, leveraging continuous, CLI-assisted refactoring as an iterative process.
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:
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 ?
