Full démonstration of Angular resource server state management

En utilisant seulement les signal & resource / httpResource/ rxResource fourni par Angular, on peut faire une solution de server-state management optimisée, fiable, scalable et propre.

Que signifie server-state management ? Pour moi, c'est principalement permettre de connaître l'état de chargement d'une requête au backend.

C'est-à-dire quand on récupère une liste d'utilisateur, je veux savoir si c'est en train de charger, si c'est chargé, s'il y a eu une erreur...

De même, si je mets à jour un utilisateur, je veux savoir l'état de chargement de la mise à jour de mon utilisateur.

C'est grâce aux nouvelles méthodes resource introduites par l'équipe Angular que l'on va pouvoir connaître le statut de chargement de chaque action (requête) que l'on va mettre en place.

Je te présente dans un premier temps la solution minimaliste que j'ai développée & comment l'utiliser, Les limites de ce qu'il est possible de faire, et enfin le fonctionnement interne de l'outil et les problématiques que j'ai rencontré.

Comment utiliser un server-state management avec les resource d'Angular ?

  • 🔗 Works with httpResource, resource, and rxResource
  • ⚡ 100% declarative / reactive / type-safe
  • 🔄 Reacts to other resource result status
  • 🚀 Optimistic updates easily supported
  • 🚫 No external libraries
  • ❌ Not a single line of RxJS (unless you really want to)
  • ❗ Handle errors
  • 🛠️ Just 2 small helper functions

L'exemple que je vais te présenter tout au long consiste à

  • afficher une liste d'utilisateurs (id & nom),
  • permettre de mettre à jour le nom,
  • de supprimer l'utilisateur,
  • remettre à jour un utilisateur s'il y a eu une erreur lors de la mise à jour de son nom

Voici le lien de la démo, n'hésite pas à le parcourir.

C'est une situation que l'on retrouve régulièrement dans les applications web, donc j'espère qu'elle va te parler.

Pour utiliser la solution, on aura besoin de quelques fonctions maisons qui permettent de profiter d'un outil type-safe, déclaratif & réactif.

Ça évite surtout d'écrire du code boilerplate, et ça permet à TS d'avoir un cadre pour profiter de sa typesafety.

Si ce n'est pas déjà fait, importe le fichier: signal-server-state.ts

Étape 1 - Définir la structure de son state & les actions

Dans un premier temps, il faut décrire d'un point de vue typage la structure que l'on souhaite de notre state, ainsi que lister les actions que l'on va appliquer.

Pour l'exemple, je créer le type UsersState en utilisant la fonction générique ServerStateContext.

UsersState va être utilisé par les fonctions que l'on appellera après, et permet de s'assurer du typage cohérent dans l'ensemble des déclarations. C'est un coup de main nécessaire pour TS pour ne pas qu'il se perde (et c'est pas une blague)

Create users state type

Étape 2 -Déclarer son state et les actions

Pour créer son state, il suffit d'utiliser la fonction mère signalServerState, sans oublier de lui passer en type parameter notre UsersState et notre état initial.

Il reste à définir les actions, tout en utilisant la méthode action. T'en fais pas, TS va te guider et te râler dessus si ça ne convient pas.

Create a custom signalServerState

Comme tu peux le voir dans l'exemple, j'utilise la méthode resource d'Angular pour récupérer ma liste d'utilisateurs via l'action GET.

Attention à bien utiliser request: () => ..., cela permet de trigger la requête de loader quand les signal au sein de la fonction request changent. Sinon ça ne fonctionne qu'une fois.

Retourne undefined dans la request () => ... return undefined; ..., quand tu souhaites que l'action ne se trigger pas. Quand la request retourne undefined, le loader n'est pas appelé.

Étape 3 - Définir tes reducer (comment l'action affecte ton state)

Define custom reducer per action

Les reducer vont être appelé à chaque fois que l'action va être déclenché.

Quand une action est trigger, ton reducer va être appelée 2 fois, une fois pendant le chargement, et une fois lorsque la requête a terminée ou échouée.

L'outil n'active pas les reducer quand la resource à pour statut Idle

Dans l'exemple, du GET, j'affecte les users s'ils ont été chargés au state et je mets à jour le statut de GET dans le state.

Voici un autre exemple pour l'action d'update:

Demonstration of implementation of UPDATE action

  • La source de mon action update est un signal

Signal server-state demonstration sources

Je fais en sorte de retourner undefined lorsque je ne veux pas trigger mon action d'update.

  • Dans le reducer, je mets à jour la liste des utilisateurs de façon optimiste, et je mets à jour le status de la resource.
  • Toujours dans le reducer, je fais en sorte de bloquer les prochains trigger de l'UPDATE via isUpdateDisable pendant qu'elle est en train de charger.

Les resources ont une stratégie de requête similaire au switchMap de RxJs. Si on lance une seconde requête avant de la première ne soit pas finie, cela va annuler la première requête

Je te présente l'action DELETE qui est similaire aux précédentes et qui permet de supprimer de la liste un utilisateur, une fois que la requête est terminée.

Demonstration of implementation of UPDATE action

Il me reste à te présenter l'action REFRESH qui est un peu particulière, car elle se déclenche via un "event" interne au state, quand l'action d'update retourne une erreur.

Défini la source d'une action par rapport à un évènement interne à ton state.

Le système de server-state que je t'ai présenté est déjà pas mal et couvre beaucoup de cas d'utilisation, mais qu'en est-il du cas où l'on veut remettre à jour un utilisateur lorsque la requête d'UPDATE est partie en erreur ?

Pour ce cas bien particulier, mais courant, j'ai mis à disposition un objet storeEvents qui expose des signal qui vont être mis à jour à chaque fois que l'action correspondante va être déclenché et changer de statut.

Use storeEvents signal for internal action source

storeEvents.UPDATE()?.status() me permet donc de connaître le statut de l'action UPDATE et s'il est en erreur, je retourne le signal source updateItem qui contient le dernier utilisateur qui a été mis à jour et parti en erreur.

Je crois fortement que pouvoir réagir à un évènement d'une autre action, en permettant de lancer une action en conséquence est indispensable pour une solution de server-state et garder un code simple et déclaratif.

Bonus: L'outil expose aussi les events du store, cela peut être pratique pour réagir dessus. Par exemple ou afficher une notification quand une update est terminée, quand il y a eu une erreur...

A la fin en utilisant les 2 fonctions que l'on a vues et la fonction générique pour créer le type du state, on arrive à une solution qui est pour moi

  • simple d'intégration (et j'espère que pour toi aussi maintenant),
  • scalable (on peut ajouter autant d'action que l'on souhaite),
  • déclarative (depuis la déclaration de l'action, on connaît la source, la requête, la façon dont le résultat va modifier le state)
  • fiable & optimisée (se déclenche seulement quand une source change, et quand on a le résultat de la requête)

Est-ce quand même possible d'utiliser des opérateurs RxJs ?

Je te vois venir, tu fais mumuses avec cet outil, en te disant que RxJs c'est finito. Puis arrive un jour, où en fait, eh bien, comment dire, il te faudrait utiliser RxJs.

Rassure-toi chèr ami, c'est fort probable que cette situation va t'arriver plus d'une fois, mais j'ai une solution pour toi !

Voici un exemple sur stackblitz, qui reprend le même fonctionnement et qui ajoute quelques operateurs RxJs pour plus de personnalisation

Dans l'exemple:

  • Il y a un debounceTime sur l'action d'UPDATE, c'est-à-dire que l'appel API va se faire une fois que la dernière action de mise à jour a attendu 2s. (Identifié // Case 1 ...dans le code)

Using RxJs operator with signal action source 1

Using RxJs operator with signal action source 2

  • Il y a aussi un debounceTime de 3s sur l'action de REFRESH (qui elle a comme source initiale une erreur de l'action d'UPDATE. (Identifié // Case 2 ... dans le code).

Using RxJs operator with inner store signal event

Pense à activer la checkbox pour forcer l'erreur API des UPDATE pour voir le mécanisme en action.

Ces patterns permettent d'avoir un outil qui scale plutôt bien au besoin, qui commence simple et s'adapte correctement pour un besoin plus poussé.

D'autre part, la fonction rxResource permet de retourner un observable plutôt qu'une promise depuis le loader, ce qui permet aussi l'ajout de certains opérateurs RxJs.

Cette solution ne permet pas non plus de tout faire comme on va le voir dans les limites.

Les limites de l'outil

Bien que l'outil soit déjà très pratique, il y a quand même des limitations à son usage, ou encore des points d'améliorations possibles :

  1. Si la source d'une resource change cela va stopper la requête précédente en train de s'exécuter. (Comportement du switchMap en RxJs). Avec la structure actuelle de l'outil, je ne pense pas que ce soit possible de changer ce comportement.

  2. L'outil n'est pas encore bien adapté lorsque l'on utilise du temps réel (websocket, SSE). Dans ces cas-là, on n'a pas besoin de connaître l'état de chargement d'une requête, donc les actions du type resource ne sont pas appropriées.

Il est possible d'améliorer de prendre en comportement avec quelques modifications, si cela t'intéresse dit le moi en commentaire.

  1. Il n'est pas possible appliquer la même action en parallèle, par exemple si on veut mettre à jour le nom de plusieurs utilisateurs à la suite. Déclencher une action avant que la précédente ne soit pas fini va forcément "annuler" la précédente.

Il est possible de créer ce genre de comportement, mais plus complexe à mettre en place. Là où il est très facile de le faire avec RxJs.

  1. L'outil tel qu'il est ne permet pas de composer son state, un peu comme le fait le @ngrx/signal-store avec les withFeature.

C'est aussi possible à faire, mais ça rajoute une couche de complexité que je souhaite éviter pour cette démo. Fais moi signe si tu veux en savoir plus.

  1. On pourrait encore améliorer le typage du type retourné par le reducer, ici on peut rajouter des propriétés, ça peut induire en erreur.

Il est aussi possible de corriger ce comportement. Fais-le-moi savoir si tu en as besoin.

Je vais maintenant, rentrer dans le coeur du code du signal-server-state.ts. Tu vas voir comment j'ai pu mettre en place ce mécanisme sans RxJs.

Comment fonctionne cet outil de server-state management basé sur les signal

  1. Une première problématique consiste à pouvoir récupérer le state en cours pour pouvoir le modifier avec un reducer quand une action est lancée.

Avec RxJs, c'est exactement ce que permet de faire l'opérateur scan qui récupère la dernière valeur émise et permet de la transformer avec la nouvelle valeur d'entrée.

Si tu veux un exemple du même genre de server-state qui utilise scan voici un article sur le sujet: Mon pattern préféré pour gérer un server-state proprement avec RxJs

Mais ce n'est pas tout, l'utilisation uniquement des signal pose 2 problématiques qui ne se posent pas avec un pattern basé sur l'event driven géré par RxJs.

  1. Comment créer un événement à partir d'un signal qui représente un état ?
  2. Comment bloquer la requête API tant que la source d'une action n'est pas valide ?

  3. Une quatrième problématique est comment partager les events internes du store (UPDATE loading, loaded, error). Pour pouvoir les utiliser comme source ?

Je vais détailler tout ça.

Récupérer le state en cours pour pouvoir le modifier avec un reducer

C'est la base d'un store, permettre de modifier le state interne quand une action est appliqué en utilisant le reducer associé.

J'ai d'abord songé à utiliser uniquement un pattern déclaratif pour créer cet outil, mais ce n'est pas possible avec les signal (là où c'est tout à fait possible en RxJs).

La solution que j'ai trouvé est de créer un signal qui va contenir le state au sein de la fonction signalServerState.

inner state signal

Il reste à savoir comment le mettre à jour lorsqu'une action se produit.

Comment créer des événements à partir d'un signal pour mettre à jour le state

J'ai créé un effect pour chaque action qui permet d'observer le changement de chaque action en observant le statut du signal resource.

Create events based on signal using effect

Pour chaque changement de statut d'une signal resource, je mets à jour le state en utilisant le reducer de l'action.

Grâce à cette solution impérative, on peut créer un système d'event.

Comment bloquer la requête API tant que la source d'une action n'est pas valide ?

Les actions comme un update, sont des événements qui doivent attendre d'être trigger pour lancer un appel API.

On vient de voir qu'on a réussi à créer des events basés sur la resource des actions, mais comment faire pour que l'appel API de la resource s'effectue uniquement quand la source de l'action change ?

J'ai un déjà mentionné la solution, pour éviter de trigger l'appel API de la resource il faut faire en sorte que la fonction request de la resource renvoie undefined.

Mais la resource change quand même de valeur et son statut passe en Idle, ce qui déclenche l'effect associé.

Pour éviter que le reducer soit appelé, j'ai rajouté une condition qui vérifie le status de la resource et stope l'éxécution si le statut est Idle.

Prevent calling the reducer action when request is Idle

Ce système permet de mettre à jour notre state uniquement quand une action est en cours de chargement, ou est chargée ou est en erreur. C'était tout l'objectif de ce server-state management.

Comment une action peut-elle avoir comme source un event d'une autre action ?

Pour permettre à une action de réagir à des events provenant d'autres actions, pas le choix, il faut créer des signal interne dès la création du store.

Create internal action sources

De cette façon, ils vont pouvoir être exposés aux actions lors de leur création.

Share action internal source

Use store source as action source

La limite lié au typage est qu'on ne peut pas savoir à ce moment là, le type renvoyé par la resource (ce que renvoie l'appel API). Une solution pourrait être de mapper l'action avec le type de Data qu'il retourne directement dans le ServerStateContext. Mais je trouve ça trop contraignant et ça perd en DX.

Mon avis dessus

Bien que je te le présente, je n'ai pas encore eu l'occasion de le tester en production. Il pourra être nécessaire de faire quelques adaptations.

Je trouve l'outil vraiment pratique pour gérer déjà un grand nombre de situations et son côté déclaratif permet d'être clair sur nos intentions dans le code.

L'outil est un petit fichier qui contient toute cette logique, et je pense que c'est une force, car même si tu es novice, son accès reste abordable pour faire évoluer l'outil. Et c'est sans doute plus facile pour l'introduire en équipe.

Je trouve que la DX est pénalisée quand on a besoin d'utiliser des opérateurs RxJs et on perd aussi en lisibilité. Mais j'ai trouvé une solution qui pourrait me convenir à 100% !

Je me suis dit pourquoi ne pas garder exactement l'outil que je viens de te présenter, qui est idéal pour démarrer. Puis si le besoin évolue, il suffira de passer sur un outil similaire, c'est-à-dire compatible avec la solution 100% signal, mais qui intègre cette fois-ci l'utilisation des observables comme source d'une resource.

Ça sera une solution hybride signal & observable qui permettra l'ajout naturel d'opérateur RxJs.

Pour cela, je vais avoir besoin de créer une resource personnalisé qui renvoi la même chose que ces copines, mais qui accepte une source d'observables.

Et là, l'évolution serait folle et la scalabilité meilleur.

N'hésite pas à me suivre pour être au courant de sa sortie. Je pense que cet outil restera exactement le même, mais je vais peut-être changer le nommage des fonctions.

J'ai hâte de sortir cet outil hybride, pour pouvoir l'utiliser sans limite.

Je travaille aussi sur un outil de server-state-management qui m'a grandement inspiré cet article et qui pousse la réflexion encore plus loin.

Gérer le CRUD sur une liste de données avec Angular ? Voici l’outil que me manquait

Si l'article t'a plu ou si tu as des questions, n'hésite pas à laisser un commentaire ou encore à me suivre sur linkedin Romain Geffrault.

Pour terminer, je dois t'avouer que je n'ai encore jamais eu l'occasion de travailler avec les nouvelles méthodes resourced'Angular, mais ça ne devrait pas tarder surtout avec cet outil.