Smart Components vs Dumb Components : Reprendre le contrôle de ses composants Angular

Partager
Smart Components vs Dumb Components : Reprendre le contrôle de ses composants Angular

Si vous avez déjà travaillé sur une application Angular de grande envergure, vous avez forcément croisé ce fichier : un composant de plus de 1200 lignes, devenu le centre névralgique d'une fonctionnalité, que tout le monde craint de modifier.

Au début, tout part d'une bonne intention. On crée un composant pour afficher un tableau de données, puis on y ajoute un bouton de suppression, alors on implémente la méthode HTTP directement dedans. Ensuite, on a besoin d'une modale de confirmation, et on ajoute la logique d'ouverture et de fermeture. Petit à petit, le composant devient progressivement un composant "fourre tout".

Cette approche montre très vite ses limites en production. Voyons pourquoi le composant "Monstre" est le pire ennemi de votre base de code, et comment l'architecture Smart vs Dumb (ou composants conteneurs et composants de présentation) va sauver votre maintenabilité.

La douleur du composant "Monstre" : Un frein pour la scalabilité

Qu'est-ce qui caractérise ce fameux composant trop riche ? C'est un élément qui prend tout à sa charge :

  • Il effectue directement les appels HTTP (fetching, modification, suppression d'éléments).
  • Il gère l'état global de la vue (tri, filtres, pagination).
  • Il orchestre l'UI locale (affichage, masquage et état des modales).

En agissant ainsi, on crée une interdépendance disproportionnée. Le composant devient tellement collé à ses données et à ses effets de bord qu'il devient extrêmement difficile à réutiliser. Pire encore, si une autre partie de l'application a besoin d'afficher la même liste de données mais dans un contexte légèrement différent, la conséquence est inévitable : on duplique le composant sur plusieurs autres pages. On se retrouve alors à maintenir trois ou quatre versions du même code visuel, avec tous les risques de désynchronisation que cela comporte.

En ingénierie logicielle, lorsqu'une classe ou un composant fait trop de choses, c'est un excellent indicateur qu'il faut séparer les responsabilités. Un composant ne devrait avoir qu'une seule raison de changer. S'il doit changer parce que l'API HTTP a évolué ET parce qu'on veut modifier le design de la modale, c'est qu'il y a un loup architectural.

Le remède : L'architecture Smart vs Dumb

Pour résoudre ce problème, on divise nos composants en deux catégories bien distinctes, ayant chacune un rôle unique et un périmètre défini.

Avant d'entrer dans le détail, voici le flux de données que l'on cherche à mettre en place :

Diagramme du flux de données du pattern Smart vs Dumb : la ProductFacade fournit les données au ProductListComponent (Smart), qui les distribue aux composants ProductCard et Filters (Dumb) via les inputs ; les événements remontent des Dumb vers le Smart via les outputs.

En un coup d'œil, on lit les quatre principes du pattern :

  • Les données descendent via les input : de la Facade vers le Smart, puis du Smart vers les Dumb.
  • Les intentions remontent via les outputs/événements : les Dumb notifient le Smart sans jamais agir eux-mêmes.
  • Le Smart orchestre : il fait le lien entre la source de données et les composants de présentation.
  • La Facade gère les données : elle centralise la logique métier et expose un état propre.

1. Les Smart Components

Le composant "Smart" (ou conteneur) est le chef d'orchestre. Il ne se soucie pas de savoir si le tableau est affiché en bleu ou en rouge, ou si la modale a une animation de fondu. Son rôle est purement logique et organisationnel.

  • Ses responsabilités : Injecter les services, orchestrer la récupération des données, interagir avec le store d'état applicatif, et attraper les événements métiers pour déclencher les modifications ou les suppressions.
  • Sa caractéristique clé : Son template HTML est très léger. Il sert principalement de point d'ancrage pour appeler les composants Dumb en leur passant les données nécessaires via leurs inputs.

Un piège classique est de surcharger le Smart component avec de la logique de tri, de filtrage ou de transformation. C'est une erreur : il doit rester un chef d'orchestre, pas un calculateur.

La règle d'or est de déléguer la logique métier à un service (souvent sous forme de Facade ou de Service d'État) : le service traite, transforme, appelle l'API si nécessaire, et expose un flux propre (Observable ou Signal) que le Smart component consomme et transmet directement au Dumb component.

Cette délégation centralise la logique métier, la rend réutilisable partout dans l'application, et simplifie les tests : un Smart component se teste en mockant uniquement le service, sans se préoccuper du visuel.

Et où va le Store dans tout ça ? Si vous utilisez une solution de gestion d'état (NgRx, Signal Store, Component Store, NGXS…), une question revient toujours : le Store remplace-t-il les Smart Components ? Non. Le Store ne les remplace pas : il leur fournit les données et expose les actions métier. Le Smart Component, lui, reste responsable de l'orchestration de la vue et de la communication avec les composants de présentation. En pratique, le Store (ou la Facade qui l'encapsule) joue le rôle de source de vérité, et le Smart Component reste le point d'ancrage qui sélectionne l'état pertinent, dispatche les actions et distribue les données aux Dumb Components.

2. Les Dumb Components

Le composant "Dumb" (ou de présentation) est un exécutant pur. Il est totalement agnostique de la provenance des données. Il reçoit des informations, les met en forme visuellement, et propage des intentions sous forme d'événements.

  • Ses responsabilités : Afficher l'interface utilisateur, gérer les interactions purement visuelles locales (état d'un panneau, ouverture d'un menu, animation…) sans embarquer de logique métier, et s'assurer que le rendu visuel est impeccable.
Attention à ne pas confondre interaction visuelle et logique métier. Toutes les modales ne se valent pas : une modale de confirmation de suppression déclenche une action métier (l'appel HTTP de suppression) — son ouverture et sa validation relèvent donc du Smart component, qui orchestre le flux. À l'inverse, une modale purement informative ou un panneau dépliable sans conséquence métier peuvent être pilotés localement par le Dumb component. La question à se poser : « cette interaction déclenche-t-elle une conséquence métier, ou reste-t-elle du pur affichage ? ».
  • Sa caractéristique clé : Il n'injecte aucun service dédié à la récupération de données. Piloté exclusivement par ses entrées et ses sorties, il se teste sans aucun mock, en injectant simplement des inputs statiques et en vérifiant le rendu.

Voici concrètement à quoi ressemble cette séparation en code, avec les APIs modernes d'Angular.

La touche moderne (2026) : L'impact des Signals et de la stratégie OnPush

L'architecture Smart/Dumb prend une toute autre dimension avec les évolutions récentes d'Angular.

1. Des Dumb Components d'une pureté absolue grâce aux Signal Inputs

Avant, on utilisait les décorateurs @Input(). Aujourd'hui, les Signal Inputs (input()) et la nouvelle API output()rendent l'écriture des Dumb components extrêmement robustes.

import { Component, ChangeDetectionStrategy, computed, input, output } from '@angular/core';

@Component({
  selector: 'app-product-card',
  standalone: true,
  templateUrl: './product-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductCardComponent {
  product = input.required<Product>();
  addToCart = output<Product>();

  // Signal dérivé : mise en forme locale, aucune logique métier
  // utilisé dans le template : {{ discountLabel() }}
  discountLabel = computed(() =>
    this.product().discount > 0 ? `−${this.product().discount}%` : null
  );

  onSelect() {
    this.addToCart.emit(this.product());
  }
}

​Les entrées sont désormais en lecture seule et s'intègrent parfaitement dans le système de réactivité d'Angular.

Les Signal Inputs empêchent de remplacer accidentellement la valeur reçue depuis le composant parent (un this.product.set(...) est impossible à la compilation), ce qui évite d'innombrables effets de bord.

Attention toutefois : cette protection ne s'étend pas au contenu d'un objet mutable — rien n'empêche techniquement un this.product().name = '...'.

La bonne pratique reste donc de traiter les données reçues comme immuables et de ne jamais les muter dans le Dumb component.

Notez l'usage de computed() : il permet de dériver de l'état local (ici, formater un label d'affichage) sans jamais sortir du périmètre du rendu visuel.

2. Le Smart Component moderne avec toSignal()

Le pendant naturel du Dumb component ci-dessus est un Smart component qui récupère les données via une Facade et les lui transmet. Le pont entre les Observables de la Facade et le template basé sur les Signals est toSignal().

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ProductCardComponent } from './product-card.component';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ProductCardComponent],
  template: `
    @for (product of products(); track product.id) {
      <app-product-card
        [product]="product"
        (addToCart)="onAddToCart($event)"
      />
    }
  `
})
export class ProductListComponent {
  private productFacade = inject(ProductFacade);

  // L'Observable de la Facade est converti en Signal : le template est réactif sans async pipe
  products = toSignal(this.productFacade.getAll$(), { initialValue: [] });

  onAddToCart(product: Product) {
    this.productFacade.addToCart(product);
  }
}

​Le Smart Component ignore totalement la manière dont une carte produit est rendue. Le Dumb Component est complètement agnostique de leur provenance. Cette séparation nette est exactement ce que l'on recherche.

3. Des performances optimisées grâce à OnPush

Parce que le composant Dumb ne dépend que de ses entrées (ses inputs), il devient le candidat parfait pour la stratégie de détection de changement OnPush.

Angular n'ira re-vérifier et re-rendre ce composant que si l'un de ses inputs a réellement changé de référence. Avec les Signal Inputs, c'est encore plus fin : les Signals permettent à Angular de cibler précisément les parties du template dépendantes d'un état modifié, limitant ainsi les mises à jour du DOM.

Combinés à OnPush, ils réduisent significativement le travail de détection de changement. Sur une page contenant des dizaines de cartes ou de lignes de tableau, ce choix architectural offre un gain de performances significatif.

Cela dit, les gains de performances sont une conséquence intéressante, mais le bénéfice principal reste une architecture plus lisible, plus testable et plus facile à faire évoluer.

Attention à ne pas tomber dans l'excès inverse. Une erreur fréquente consiste à vouloir rendre tous les composants "Dumb". En réalité, une application Angular saine repose sur un équilibre entre composants Smart et Dumb : les premiers orchestrent les cas d'usage, les seconds se concentrent sur le rendu. Le pattern est un outil de séparation des responsabilités, pas une règle absolue à appliquer aveuglément.

Conclusion et Règle d'or pour vos Code Reviews

L'architecture Smart/Dumb n'est pas une contrainte inutile, c'est une hygiène de code indispensable pour garder une base de code saine et évolutive. Elle force l'équipe à réfléchir avant de coder : "Est-ce que ce que je développe est une règle métier, ou simplement de l'affichage ?".

La règle d'or en Code Review : Le vrai signal d'alarme n'est pas la taille du fichier, c'est le mélange des responsabilités. Dès qu'un composant combine de la mise en page fine (CSS, états visuels de boutons) avec des appels HTTP directs ou des manipulations complexes de données, il est temps de séparer les responsabilités, de créer un service pour la logique métier, et de découper le visuel en composants Dumb réutilisables. Un Dumb component de 400 lignes entièrement dédié au rendu reste infiniment plus sain qu'un Smart component de 100 lignes qui fait tout à la fois.

En appliquant ce pattern rigoureusement, vos PRs seront plus simples à relire, votre application sera beaucoup plus performante, et votre équipe gagnera un temps précieux lors des phases de maintenance et d'onboarding.

Lire la suite