Depuis l'intégration des signals dans Angular 16, la gestion d'état a bifurqué en deux chemins divergents. D'un côté, NgRx SignalStore propose une abstraction légère et opinionée bâtie sur les signals natifs. De l'autre, les services à signaux offrent une approche minimaliste, sans dépendance externe. Ces deux solutions ne répondent pas aux mêmes problèmes, et confondre leurs cas d'usage conduit à des architectures fragiles et difficiles à maintenir. Cet article démystifie les différences concrètes, les compromis de performance, et comment prendre la bonne décision selon votre contexte applicatif.
Architecture et philosophie : deux visions opposées
NgRx SignalStore est une couche d'abstraction construite par l'équipe NgRx pour normaliser la gestion d'état avec les signals. Elle fournit une structure explicite : des stores typés, des selectors réactifs, des effects intégrés et une API déclarative pour les mutations. Chaque store est une classe annotée avec @signalStore() qui expose des signals publics, des computed signals pour les dérivations, et des methods pour les actions. Cette approche rappelle Redux ou Vuex, mais allégée et sans middleware lourd. À l'inverse, un service à signaux est juste une classe ordinaire contenant des signals privés et exposant des getters ou des methods pour les manipuler. C'est minimaliste : pas de convention, pas de décorateur, pas d'infrastructure. Un développeur crée un UserService avec un signal $user et c'est tout. Cette différence philosophique explique pourquoi les deux approches ne sont pas interchangeables.
Quand NgRx SignalStore fait sens
NgRx SignalStore excelle dans les applications moyennes à grandes, avec une logique métier complexe et plusieurs domaines d'état. Imaginez une application e-commerce : vous avez un store pour les produits, un pour le panier, un pour les commandes, un pour l'authentification. Chacun de ces domaines peut avoir des selectors composables, des effects asynchrones (appels API), et des mutations conditionnelles. SignalStore permet de structurer cela de manière cohérente et testable. Les effects intégrés facilitent la gestion des side effects : charger les produits au démarrage, valider un code promo, persister le panier. Vous bénéficiez aussi de l'écosystème NgRx : Redux DevTools pour le debugging, les patterns établis, et une communauté active. Si votre équipe a déjà utilisé NgRx classique (avec ngrx/store), la migration vers SignalStore est progressive. Un autre cas d'usage : les applications avec du state partage complexe. Quand plusieurs composants distants doivent réagir aux mêmes données, et que ces données se transforment différemment selon le contexte, SignalStore fournit une source unique et fiable.
Quand un service à signaux suffit
Les services à signaux brillent dans les petites applications, les micro-applications ou les domaines métier isolés. Prenez une todo list simple, un panier de e-commerce minimaliste, ou un widget d'authentification. Un TodoService avec un signal $todos, une method addTodo(), et un computed signal $remainingCount suffit amplement. Aucune infrastructure, aucune dépendance externe, aucune courbe d'apprentissage. Le code est immédiatement lisible pour un junior. Les services à signaux sont aussi préférables quand vous avez besoin de flexibilité locale : un formulaire dynamique qui maintient son propre état, une liste avec filtrage côté client, une modale avec sa logique interne. Ces composants n'ont pas besoin d'une normalisation globale. Enfin, si vous construisez une librairie de composants Angular (design system), exposer des services à signaux plutôt que de dépendre de NgRx rend votre librairie plus légère et adaptable.
Comparaison de performance et d'empreinte
Sur le plan du bundle, un service à signaux n'ajoute rien : c'est du code Angular natif. NgRx SignalStore ajoute environ 15-20 KB gzippé (version 18+), ce qui n'est pas énorme mais mérite d'être considéré dans une PWA. Sur le plan de la réactivité, les deux approches sont équivalentes : elles reposent sur les signals, donc détection de changements granulaire et pas de zone.js par défaut. Cependant, SignalStore permet une meilleure optimisation dans les grandes applications : les selectors mémoïsés évitent les recalculs inutiles, et les effects isolent les side effects. Dans un service à signaux mal structuré, on voit souvent des developers créer trop de computed signals ou des watchers qui se déclenchent en cascade. La vraie différence de performance émerge en production : une application SignalStore bien conçue scale mieux qu'une collection de services à signaux hétérogènes, car l'architecture force la discipline.
Un exemple concret : gestion d'un panier
Avec SignalStore, un panier ressemblerait à ceci : un store typé avec des signals pour les items et le total, des computed signals pour le nombre d'articles et la TVA, des methods pour ajouter/supprimer un article, et un effect pour persister les changements. Le code est auto-documenté et testable en isolation. Avec un service à signaux, vous créeriez un CartService avec un signal $items, des methods addItem() et removeItem(), et un effet inside la method pour sauvegarder. C'est plus court, mais si le panier devait aussi gérer les codes promo, les remises, la synchronisation multi-onglet, et les tentatives de paiement, le service deviendrait rapidement un monolithe difficile à tester.
Le piège courant : sur-architecting avec SignalStore
Beaucoup d'équipes adoptent NgRx SignalStore pour des applications qui ne le justifient pas. On voit des petits projets avec 5 stores complexes pour gérer 3 formulaires simples. Le résultat : plus de code à maintenir, une courbe d'apprentissage inutile, et une complexité qui ralentit les onboardings. À l'inverse, certains projets grandissent progressivement et se retrouvent avec une dizaine de services à signaux dupliqués et incohérents. La bonne stratégie : commencez simple avec des services à signaux, et migrez vers SignalStore seulement quand vous identifiez du code dupliqué, des difficultés de testabilité, ou des side effects qui s'entrelacent. Cette approche incrémentale réduit les regrets architecturaux.
Cohabitation et patterns hybrides
Il n'est pas rare de voir SignalStore et services à signaux cohabiter. Par exemple, un store global gère l'authentification et la navigation, tandis que des services à signaux gèrent l'état local des formulaires et des listes filtrées. C'est une approche saine : vous centralisez ce qui a besoin de l'être, et vous gardez le reste local. Les patterns hybrides permettent aussi une migration progressive : remplacez un service à signaux par un store SignalStore quand la complexité justifie l'investissement.
Conclusion opérationnelle
Choisir entre NgRx SignalStore et un service à signaux n'est pas une question de préférence personnelle, mais d'adéquation au problème. Posez-vous ces questions : l'état doit-il être partagé entre plusieurs domaines métier ? Y a-t-il des side effects complexes et imbriqués ? L'équipe maîtrise-t-elle les patterns Redux ? Si oui, SignalStore. Si non, si l'état est local et simple, un service à signaux suffit. Dans la majorité des cas, une application moderne Angular bénéficie d'une combinaison : du SignalStore pour l'état global, des services à signaux pour le local. Mesurez votre complexité réelle avant d'ajouter une dépendance, mais ne laissez pas cette minimalisme initiale vous bloquer quand l'architecture devient incontrôlable. Angular offre les outils pour évoluer progressivement.