We’re going to examine how to refactor a single feature component that is doing everything into two distinct parts: a smart component and a dumb component.
We’ve touched upon this architectural approach in a previous video, which can be referenced for more depth.
This article is adapted from the following video:
Understanding Smart and Dumb Components
The strategy of breaking down components into smart feature components and dumb presentational components helps improve the architecture of our apps.
A smart component is usually a routed component, like a page, that handles several tasks like injecting services, managing observables, and complex logic. On the other hand, a dumb or presentational component mainly focuses on displaying data. It remains oblivious to the app’s overall structure and communicates with the rest of the app through its inputs and outputs.
Let’s consider a typical example of an Ionic page component. This component is responsible for injecting the store, triggering a load, and managing how to display that data from the store. You could call this a smart component - but it’s a smart component that is responsible for too much.
client-dashboard.pages.ts
export class ClientDashboardPage implements OnInit {
constructor(public clientsStore: ClientsStore) {}
ngOnInit() {
this.clientsStore.loadClients();
}
}
client-dashboard.pages.html
<ion-content>
<ion-list date-test="list">
<ion-item *ngFor="let client of clientsStore.clients$ | async">
<ion-avatar slot="start">
<img src="http://gravatar.com/avatar/" />
</ion-avatar>
<ion-lable>{{ client }} </ion-lable>
</ion-item>
</ion-list>
</ion-content>
The issue of complexity may not seem evident when we’re just dealing with a simple list display, but as the template complexity increases, managing everything through one component becomes challenging. Also, the code ends up being tightly coupled or strongly connected to that single component.
Refactoring the Component
Now, let’s refactor the component to make it more manageable and loosely coupled.
client-list.component.ts
export class ClientListComponent {
@Input() clients: Client[];
constructor() {}
}
client-list.component.html
<ion-list data-test="list">
<ion-item *ngFor="let client of clients">
<ion-avatar slot="start">
<img src="https://gravatar.com/avatar/" />
</ion-avatar>
<ion-label>{{ client }} </ion-label>
</ion-item>
</ion-list>
We’ve shifted all the responsibility of displaying the list to a new client-list
component, which serves as the dumb or presentational component. Now the smart component only needs to add this dumb component to the template and supply the data through an input.
<ion-content>
<app-client-list *ngIf="clientsStore.clients$ | async as clients" [clients]="clients"></app-client-list>
</ion-content>
It’s crucial to note that the smart component still manages the access to the store and deals with the observable. The async pipe is still being used to retrieve the value from the observable stream, which is then passed to our client-list
component as a normal synchronous value.
As for our dumb component, it merely takes an input for clients and renders the same list as before in the smart component. If we want to perform an action when a client is clicked, we can set up an output. This allows the client-list
component to notify the parent component when a specific item is clicked. Remember, we don’t want the dumb component to trigger any method in a service or navigation; this should be the smart component’s responsibility.
Even for a simple template like this one, the benefit of this approach soon becomes clear. The smart component’s template is now more straightforward, and the client-list
component can be easily reused elsewhere, as it doesn’t depend specifically on the client-dashboard
.
Conclusion
While this simplified example may not fully demonstrate the real-life benefits of this approach, remember that as an application grows in complexity, this strategy proves to be more and more advantageous.