Maîtrisez le pattern Smart/Dumb components : container vs presentation, data binding, communication parent-enfant et architecture scalable.
Introduction
Le pattern Smart/Dumb Components (aussi appelé Container/Presentational) est une architecture qui sépare la logique métier de la présentation. C'est une excellente façon de créer des applications maintenables, testables et réutilisables.
Smart Components — Les conteneurs
Smart Components (ou Container Components) orchestrent la logique métier. Ils :
- ✅ Gèrent la logique métier et l'état
- ✅ Communiquent avec les services (HTTP, state management)
- ✅ Passent les données aux Dumb Components via @Input
- ✅ Écoutent les événements des Dumb Components via @Output
- ❌ Ne contiennent pas ou très peu de HTML
// user-list.container.ts — Smart Component
import { Component, OnInit } from '@angular/core';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-user-list',
template: `
<div class="loading" *ngIf="isLoading">Chargement...</div>
<app-user-list-presentation
[users]="users"
[selectedUserId]="selectedUserId"
(userSelected)="onUserSelected($event)"
(userDeleted)="onUserDeleted($event)">
</app-user-list-presentation>
`
})
export class UserListContainerComponent implements OnInit {
users: User[] = [];
selectedUserId: number | null = null;
isLoading = false;
constructor(private userService: UserService) {}
ngOnInit() {
this.loadUsers();
}
private loadUsers() {
this.isLoading = true;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.isLoading = false;
},
error: (err) => {
console.error('Erreur :', err);
this.isLoading = false;
}
});
}
onUserSelected(userId: number) {
this.selectedUserId = userId;
}
onUserDeleted(userId: number) {
this.userService.deleteUser(userId).subscribe(() => {
this.users = this.users.filter(u => u.id !== userId);
});
}
}
Dumb Components — Les présentationnels
Dumb Components (ou Presentational Components) ne font qu'afficher. Ils reçoivent les données et émettent des événements. Zéro logique métier :
- ✅ Reçoivent les données via @Input
- ✅ Émettent les événements utilisateur via @Output
- ✅ Ne connaissent pas les services (pas d'HttpClient, pas d'injectables)
- ✅ Sont 100% réutilisables dans d'autres contextes
- ✅ Faciles à tester (pas de dépendances externes)
// user-list.presentation.ts — Dumb Component
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-user-list-presentation',
template: `
<div class="user-list">
<div *ngFor="let user of users"
class="user-item"
[class.selected]="user.id === selectedUserId"
(click)="selectUser(user.id)">
<h4>{{ user.name }}</h4>
<p>{{ user.email }}</p>
<button (click)="deleteUser(user.id); $event.stopPropagation()">
Supprimer
</button>
</div>
</div>
`
})
export class UserListPresentationComponent {
@Input() users: User[] = [];
@Input() selectedUserId: number | null = null;
@Output() userSelected = new EventEmitter<number>();
@Output() userDeleted = new EventEmitter<number>();
selectUser(userId: number) {
this.userSelected.emit(userId);
}
deleteUser(userId: number) {
if (confirm('Êtes-vous sûr ?')) {
this.userDeleted.emit(userId);
}
}
}
it('should emit userDeleted when delete button clicked', () => {
spyOn(component.userDeleted, 'emit');
component.users = [{ id: 1, name: 'John' }];
component.deleteUser(1);
expect(component.userDeleted.emit).toHaveBeenCalledWith(1);
});
Communication Smart ↔ Dumb
Les données circulent selon un flux unidirectionnel :
| Direction | Mécanisme | Exemple |
|---|---|---|
| Smart → Dumb | @Input() |
[users]="users" |
| Dumb → Smart | @Output() EventEmitter |
(userDeleted)="onDelete($event)" |
| Smart → Service | Dependency Injection |
constructor(private api: ApiService) |
Structure de projet recommandée
Organisez vos composants par feature :
src/app/
features/
users/ # Feature module
├── containers/ # Smart Components
│ └── user-list/
│ ├── user-list.container.ts
│ └── user-list.container.html
├── components/ # Dumb Components
│ ├── user-card/
│ │ ├── user-card.component.ts
│ │ ├── user-card.component.html
│ │ └── user-card.component.spec.ts
│ └── user-form/
├── services/
│ ├── user.service.ts
│ └── user.service.spec.ts
├── models/
│ └── user.model.ts
└── users.module.ts
*.container.ts→ Smart Component*.component.ts→ Dumb Component- Cela rend l'architecture visible au premier coup d'œil
Conclusion
Smart/Dumb Components crée une architecture claire, maintenable et testable. Les bénéfices :
- ✅ Clarté : Qui gère la logique ? Qui affiche ? C'est explicite
- ✅ Réutilisabilité : Les Dumb Components s'utilisent partout
- ✅ Testabilité : Pas de dépendances = tests plus simples
- ✅ Performance : Les Dumb Components changent peu = moins de détection
- ✅ Équipes : Une équipe gère la logique, l'autre la présentation