Smart and Dumb (Presentational) Components in Angular
We already have some idea of the what components are all about. We have discussed how our applications are essentially a tree of nested components starting from our root component.
Our root component might use the <router-outlet> to control which page
components are displayed (which are just standard components that are serving
the role of displaying some “page” like the home page or the settings
page). Those components might then contain additional components that serve
various roles — like displaying a form and accepting user input, or displaying
a search bar, or displaying a list with items that can be clicked.
What is not immediately obvious is how we should go about deciding when to create a component and what it should be. Given the scenario above we could create a:
HomeComponentpage
and within that page we could have three additional components:
SearchBarComponentFormComponentListComponent
Or… we could just define everything directly in the HomeComponent rather
than creating all these separate components. To help us with this, we are going
to revisit the single-responsibility principle and introduce the concept of
smart and dumb (or presentational) components.
The Single Responsibility Principle
As we discussed in the last lesson, the single-responsibility principle suggests that:
Every class should have only one responsibility
The general idea in the context of components is that one component should have one responsibility. This definition is vague, because we could tighten or broaden our scope of what that one responsibility is. Really, this is just something to get us thinking about the architecture — we don’t always have to strictly adhere to it, but we should always consider if breaking things up into more components might make our application more adaptable/maintainable/clean.
This will help us to determine when we should create a new component. Imagine we have a home page. Within that home page we have a header, a search bar, a card, and a list. We could just add everything we need to the home component.
The home component’s responsibilities might then look like this:
- Render all of the code required for all of these UI elements
- Set up bindings for the search bar
- Listen for changes in input on the search bar
- If there is a new search, fetch the new data
- Render the data inside of the list
- Set up event bindings for the list to detect clicks
- …and so on
We could quite comfortably say that this component has more than one responsibility. Instead, we might break this up into multiple components. Specifically, we could break this up into smart and dumb components.
Smart and Dumb Components
The idea with smart and dumb components is that we break up our components into smart components that know what is happening in the application in a broader sense, and dumb (or presentational) components that don’t know anything about the application.
What this generally means is that smart components will play the role of composing different components together, they can inject dependencies, make calls to services, request streams/signals from services and so on. A dumb component (generally) just receives inputs from its smart parent component, and its only way to communicate with the rest of the application is through its outputs. They (generally) don’t inject dependencies and they don’t make calls to services. This is why they are “dumb”, they don’t know what the goals of the application are on a broader scale, they get their inputs and they give outputs as a result (or maybe they don’t even do that). The dumb components don’t even really know why they are getting certain inputs or why they are giving certain outputs, they just do what they are supposed to do.
To give this a little more context, let’s break up that home page example we just looked at into smart and dumb components:
Smart components:
HomeComponent
Dumb components:
HeaderComponentListComponentSearchBarComponentCardComponent
A lot of the time we will have a single smart component, and that smart component is generally the component that is being routed to (i.e. the “page” component). Then, everything inside of that component is a dumb component. The smart component essentially handles “orchestrating” everything — it gets everything the dumb components need, it gives it to them, and handles any outputs from the dumb components making sure the data goes where it needs to go (e.g. maybe a service needs to be called as a result). This is usually the smart component’s one responsibility — to handle the composition/orchestration of its child components.
It is possible to have nested smart components, especially if the application is more complex, but for all of our examples we will always just have the single smart component that is the container/page component being routed to, and everything inside of it will be dumb components.
Let’s take a closer look at what our smart and dumb components mentioned above might look like.
What does a “smart” component look like?
A smart component might have a template that looks something like this:
<app-header><app-header><app-searchbar [filterControl]="filterControl"></app-searchbar><app-list [items]="items$ | async" (itemSelected)="viewItem($event)" (itemDeleted)="deleteItem($event)"></app-list>And a class that looks something like this:
export class HomeComponent {
private itemsService = inject(ItemsService);
filterControl = new FormControl();
// Apply filter values to items stream items$ = filterControl.valueChanges.pipe( switchMap((searchTerm) => this.itemsService.getItems().pipe( map((items) => items.filter((item) => item.includes(searchTerm))) )) )
viewItem(item: Item){ // view item }
deleteItem(item: Item){ this.itemsService.delete(item); }}This smart component is responsible for composing multiple dumb components
together and orchestrating the data flow between them and wherever the data
needs to go. This component “knows” stuff about the broader application. It
injects the ItemsService so it is aware that exists, and it knows that when
the itemDeleted event is triggered it should call the delete method from
that service.
I’ve intentionally included a technique in the example above that breaks the
rule about dumb components only communicating through outputs. In this case,
what we are doing is creating a FormControl in the parent smart component and
passing that to one of the dumb components as an input. When the dumb component
changes the value of that form control, we are able to get that data in the
parent smart component through the valueChanges observable of the control. We
are getting data out of the dumb component without using an output.
I included this to show that sometimes there are good reasons to break the rules, you just need to be considerate when doing it. In this case, it simplifies our code to do it this way, and it doesn’t make our dumb component any “smarter” — it just gets a form control as an input and uses it, the dumb component has no idea about anything going on when the value of that form control changes.
The alternative would be to do something like defining an output on
<app-searchbar> that emits the search term when it changes. But then our
parent smart component, if it wants to treat the search terms as a stream of
values which is more convenient, needs to manually grab a reference to the
app-searchbar component using a viewChild to access the stream of values
from the event. So, sometimes we do these “clever” things that technically break
the rules, but it’s not really breaking the “spirit” of the rule.
What does a “dumb” component look like?
Let’s continue on with the same example and consider what one of our dumb
components might look like. We will look at <app-list> since it is the most
interesting.
It might have a class that looks like this:
export class ListComponent { items = input<Item[]>(); itemSelected = output<Item>(); itemDeleted = output<Item>();}One thing that is very characteristic about dumb components is that their
classes will often look like this — just some inputs and outputs and maybe
nothing else going on. We still might have some logic implemented within a dumb
component (e.g. this ListComponent could include some methods or lifecycle
hooks), but a lot of the time the classes are very simple and often might have
literally nothing else but the inputs and outputs.
Its template might look something like this:
<ul> @for (item of items; track item.id){ <li> <a (click)="itemSelected.emit(item)">{{ item }}</a> <button (click)="itemDeleted.emit(item)">Delete</button> </li> }</ul>Consider what this component “knows” about the rest of the application. It doesn’t know anything except the inputs it gets and the outputs it creates. Its responsibility is to display a list of items.
This is a great way to organise our applications generally, but it also does
have other benefits. Since this code is not intertwined specifically with the
broader logic of where it is being used (e.g. it is not injecting the
ItemService), it is modular and can easily be used in other situations within
the same application or even in different applications. We can use this
component anywhere, and we just need to give it a list of items.