Front-end

Smart & Dumb Components : Architecture scalable

Angular Architecture Components Pattern
Smart & Dumb Components : Architecture scalable

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.

Idée fondamentale : Les conteneurs gèrent les données et la logique. Les composants de présentation ne font qu'afficher. Responsabilité unique = code plus clair.

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);
        }
    }
}
Test unitaire d'un Dumb Component :
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)
Règle d'or : Les données descendent via @Input. Les événements remontent via @Output. Les services ne sont injectés que dans les Smart Components.

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
Conventions de nommage :
  • *.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
Alternative moderne : Avec Angular 17+ Signals et reactive pattern, certains préfèrent un approche plus fluide sans distinction stricte Smart/Dumb. Mais ce pattern reste une excellente fondation.

Questions fréquentes

Un composant Smart (container) gère la logique métier, injecte des services, communique avec le store/API, et orchestre les données. Un composant Dumb (presentational) reçoit des données via `@Input()`, émet des événements via `@Output()`, et ne contient que de la logique d'affichage. Cette séparation facilite les tests et la réutilisation.

Un composant est Dumb si : il peut fonctionner sans services injectés, ses données viennent exclusivement d'inputs, et il peut être testé sans mocks complexes. Il est Smart si : il injecte des services, décide quelles données afficher, ou coordonne plusieurs composants enfants. En cas de doute, commencez par Dumb et promovez en Smart quand nécessaire.

Rarement. La réutilisabilité est l'apanage des composants Dumb. Les composants Smart sont généralement spécifiques à une page ou une fonctionnalité. Cependant, vous pouvez les configurer via `@Input()` pour différents contextes (ex: un `UserListSmartComponent` avec un input `filterType`).

Le Smart component expose des Signals via son service : `readonly users = this.userService.users`. Le Dumb component reçoit les données via `input()` Signal : `users = input([])`. La propagation est automatiquement réactive. Avec NgRx Signal Store, le Smart component injecte le store et passe les données aux Dumb via bindings Signal.

Partager