DEV Community

LcsGa
LcsGa

Posted on • Originally published at linkedin.com

Les Signals Angular ne remplacent pas les Observables : Pull vs Push

Récemment, on voit de nombreux développeurs tenter de remplacer RxJS en créant leurs propres "opérateurs" pour les signals d'Angular. Cette démarche est compréhensible, mais elle peut entraîner des erreurs subtiles. Pour les éviter, il est essentiel de bien comprendre la distinction entre deux modèles de réactivité : Push et Pull.


Push (Observables) : Chaque donnée poussée compte

Dans le modèle Push, l’émetteur (la source) est aux commandes. Il émet des valeurs dans un flux "observable", et chaque valeur émise existe indépendamment. Ces valeurs sont ensuite traitées, une par une, par ceux qui les observent.

Imaginez une chaîne de montage : chaque pièce qui passe existe indépendamment. Si vous vous placez le long de la chaîne pour observer, vous verrez chaque pièce passer, sans exception. Si vous arrivez en retard, vous avez simplement raté les premières pièces. Elles ont été envoyées, mais elles ne vous attendent pas.

👉 Ce modèle est idéal pour gérer des flux de données, où chaque valeur a son importance.


Pull (Signals) : Seule la dernière valeur tirée compte

Dans le modèle Pull, c’est le consommateur qui est aux commandes. Une valeur n’a d’intérêt que lorsqu’elle est explicitement lue. Le consommateur "tirera" la valeur au moment où il en a besoin. Cela implique que les valeurs transitoires qui ne sont pas lues n’ont aucune importance.

Imaginez un tableau d'affichage numérique dans une gare. Il est constamment mis à jour, affichant des informations en temps réel sur les trains. Il peut y avoir des dizaines de mises à jour par seconde, mais le voyageur ne verra que la dernière version affichée lorsqu’il lève les yeux. Les mises à jour intermédiaires ne comptent pas pour lui. Seule la dernière version de l’état du tableau est pertinente.

👉 Les signals sont donc parfaits pour gérer un état dont seule la dernière version compte.


Pourquoi certains opérateurs ont du sens et d'autres non

Cette distinction entre Push et Pull est cruciale pour comprendre pourquoi certains opérateurs fonctionnent avec les signals et d’autres non.

✅ Ce qui fonctionne : opérer sur l'état final.

Les opérateurs qui ne se préoccupent pas de l’historique et réagissent uniquement au changement d’état final fonctionneront bien avec les signals. Par exemple, un opérateur double est parfait pour un signal. Il lit simplement la valeur actuelle, la multiplie par deux et retourne le résultat. Il n’a besoin de rien d’autre.

const count = signal(2);
const doubledCount = computed(() => count() * 2);
console.log(doubledCount()); // 4
Enter fullscreen mode Exit fullscreen mode

❌ Ce qui ne fonctionne pas : avoir besoin de l'historique.

Les opérateurs comme filter ou take sont conçus pour travailler avec des flux de données. Les adapter aux signals est risqué. Pourquoi ?

  • Le piège de filter :

    Imaginez un signal initialisé à 1 (const counter = signal(1)) et un signal résultant (un computed par exemple) qui ne garde que les nombres pairs. Si vous faites counter.set(2), puis counter.set(3), le signal résultant ne verra jamais la valeur 2. En effet, si vous ne tirez que la valeur finale, le signal prendra uniquement en compte la dernière valeur, qui est 3 (impair), et la filtrera. Ainsi, le résultat ne sera jamais celui que vous attendiez.

    const counter = signal(1);
    const evenCounter = computed(() => {
        const value = counter();
        return isEven(value) ? value : undefined;
    });
    
    counter.set(2);
    counter.set(3);
    console.log(evenCounter()); // undefined => instead of 2
    

    À l’inverse, si la dernière valeur est 4, vous aurez la fausse impression que votre opérateur fonctionne correctement.

    const counter = signal(1);
    const evenCounter = computed(() => {
        const value = counter();
        return isEven(value) ? value : undefined;
    });
    
    counter.set(2);
    counter.set(3);
    counter.set(4);
    console.log(evenCounter()); // 4 => tricky result
    
  • Le piège de take :

    De la même manière, avec le même signal counter initialisé à 1, si vous effectuez counter.set(2), puis counter.set(3), puis counter.set(4), un opérateur take(3) ne prendra en compte que la dernière valeur (4). Il n’aura aucune idée que les valeurs 1, 2 et 3 ont existé. Pour lui, c'est sa première (et unique) itération.


En résumé

Les signals sont optimisés pour gérer l’état de l’application (modèle Pull), tandis que RxJS est parfait pour gérer des flux de données (modèle Push).

Avant d’utiliser l’un ou l’autre, posez-vous la question suivante :

"Ai-je besoin de connaître l’historique des données, ou est-ce que seule la dernière valeur m’importe ?"

Si seule la dernière valeur compte, les signals sont faits pour vous. Sinon, un observable est probablement la meilleure solution.

Top comments (4)

Collapse
 
dariomannu profile image
Dario Mannu

Avec rimmel.js, la frontière entre état local et flux de données disparaît: tout est représenté par des Observables.

Ceux-ci peuvent être utilisés aussi bien pour stocker et muter l’état que pour propager des mises à jour asynchrones. Cela évite d’avoir à choisir entre différents modèles de réactivité (Observables, Signals, stores, etc.) et garantit une approche unifiée et consistante de la gestion des données.

Collapse
 
lcsga profile image
LcsGa

Oui les observables étant très flexibles, on peut effectivement construire tout un système de state management uniquement avec eux (c'était d'ailleurs ce que quasiment tout le monde faisait avant les signals) !

Côté angular par exemple, la lib du genre la plus connue est NgRx! Elle permet de tout gérer avec des observables notamment via le package @ngrx/store.

Pour autant pour du state management, les signals restent tout indiqués et ils viennent avec leur lot d'avantages, TANT QUE l'on sait quand les utiliser et quand les observables restent la meilleure option !

À l'ère des signals, le but de l'article est justement de montrer quand utiliser laquelle de ces deux options 😁 !

PS : D'ailleurs, aujourd'hui NgRx propose également le package @ngrx/signals (qui permet, entre autres, de combiner le meilleur des deux mondes)

Collapse
 
dariomannu profile image
Dario Mannu

Je comprends ton point de vue, mais je pense que ngrx n’apporte finalement pas grand-chose de plus qu’un simple clone de Redux. Si quelqu’un souhaite vraiment écrire du code lourd et verbeux, autant utiliser Redux directement. Avec des streams purs, il n’y a pas besoin de "store" artificiel: ils encapsulent déjà naturellement l’état et les transitions.

À mon sens, l’équipe qui a conçu ngrx est passée à côté de la puissance des observables, en voulant surtout livrer rapidement quelque chose qui fonctionne dans Angular. Le problème, c’est qu’Angular lui-même reste assez limité dans sa prise en charge de ces flux.

C’est pour cette raison que j’ai mentionné Rimmel : il corrige justement ces lacunes et permet d’écrire du code de haute qualité sans tout le surpoids inutile. Pas besoin de me croire sur parole, il suffit d’essayer pour se rendre compte de la différence.

Thread Thread
 
lcsga profile image
LcsGa

@ngrx/store, c'est très précisément un redux like rxjs, pour angular :

Store is RxJS powered global state management for Angular applications, inspired by Redux.

Quand tu dis qu'on peut s'en passer : oui complètement, mais comme la grande majorité des librairies => c'est un facilitateur.
Il pose un cadre sur ton state management et c'est porté par une équipe de dev très compétents (qui ont très clairement comprit comment fonctionnent les observables ET angular et qui propose une implémentation qui tire le meilleur des deux).

Si on compare donc Angular à rimmel.js (qui à l'air vraiment top, je te l'accorde !), c'est sûr que le framework propose une moins bonne intégration (Angular se proposant juste d''unwrap les observables dans le template et rien d'autre => le reste c'est à la main). De ce fait, en se limitant à ces possibilités, je comprends que tu ais le sentiment que NgRx est passé à côté de la puissance des observables, mais en réalité c'est plutôt qu'Angular pose des limites.

Du coup, je ne peux pas dire le contraire quand tu affirmes que Rimmel corrige ce défaut, mais on compare deux technologies front-end qui n'ont pas du tout la même philosophie, et ça rend la comparaison un peu hors sujet.

Plutôt que d'intégrer les observables à la manière de rimmel, Angular a choisit de créer une nouvelle primitve reactive : les signals (bien mieux intégrés à Angular, à l'instar des observables dans rimmel).

=> Ils sont une vraie bonne solution à la gestion d'états dans nos apps (et pas que Angular, ils arrivent en natif en JS à l'instar des observables et la majorité des technos front modernes implémentent également leur propre solution équivalente).

Ils ne remplacent pas les observables pour autant, et c'est ça que j'explique dans mon article.