C'est l'outil que j'aurais aimé avoir pour ne plus perdre de temps à gérer les actions sur une liste de données avec Angular.
Le but est de pouvoir appliqué facilement des actions CRUD sur une entité de la liste ou sur les entités sélectionnées et de tracker les états de chargement.
Bien que ce soit encore un prototype et qu'il faut maintenant que je teste sur le terrain, la solution est:
-✅ Entièrement fonctionnelle
-⚡ 100% déclarative & réactive
-🧠 Type-safe (indispensable pour l'auto-complétion), tous les types sont inférés
-🚀 Performante & optimisée (compatible zoneless)
-🛠️ Facile à prendre en main et super rapide à mettre en place
-🎛️ Personnalisable, qui s'adapte aux cas les plus simples et complexes
-🌀 Bénéficie de la puissance de RxJs
-📭 Pas de boilerplate
-🧼 Pas de memoryleak
Elle permet:
-📊 de connaître l'état de chargement de la liste de données (loading, loaded, error...)
-✍️ d'appliquer une/des actions CRUD et de tracker l'état de chargement de chacune de ces actions sur chaque entité
-🤹♂️ d'appliquer en parallèle les actions sur chaque entité
-🧩 de sélectionner et d'appliquer une action sur l'ensemble des entités sélectionnées, tout en trackant au niveau de chaque entité l'état de chargement
-🧮 d'ajouter des sélecteurs (états dérivés) au niveau de chaque entité et de la liste globale
-🔄 de choisir la stratégie de requête à appliquer pour chaque action (switchMap, exhaustMap, concatMap, mergeMap)
-🧘 préserve l'affichage en cours lors du changement de page
Quand je dis action, cela peut être un appel API (ex: mettre à jour le nom d'une entité via un appel API UPDATE ou PUT).
Comme tu vas le voir par la suite, je trouve que la porte d'entré pour utilisé cet outil est assez accessible et ne nécessite que quelques connaissances basic en RxJs, au minimum connaître les Subjects.
Si tu n'as pas l'habitude des Subjects, prends 2min pour regarder ce qu'ils font. Ils suffisent pour faire tourner l'outil.
Bref, j'ai créé un outil de server state management. Je l'ai nommé DataListStore et j'en suis très fière.
Voici comment il s'utilise et je serai curieux d'avoir ton avis dessus.
Définir des sources (RxJs), pagination et actions
Ces sources vont servir pour trigger les actions/appels API que l'on va définir.
Grâce à RxJs, ces sources ne nécessitent que d'être des observables et peuvent donc provenir de "stream" existant (état dérivé).
Si tu n'as pas l'habitude avec ce concept, ne t'embête pas avec ça maintenant, les Subject suffiront.
Comment utiliser DataListStore pour afficher une liste de données ?
Il suffit d'injecter DataListStore comme un service habituel en Angular.
Le store renvoie une fonction, qui attend en paramètre la config du store.
Les paramètres étant entièrement typés, cela est très facile à utiliser grâce à l'auto-complétion.
Pour simplement afficher une liste de données et de connaître son état de chargement (loading, loaded, error), il suffit de lui donner l'appel api pour récupérer la liste de données, ainsi que la source (généralement, c'est la pagination).
Dès que la source émet une nouvelle valeur, cela va appeler l'api.
A noter qu'il faut aussi ajouter une fonction entityIdSelector
qui permet de savoir comment identifier une entité. Je ne sais pas encore si je vais la garder.
Comment ajouter des actions CRUD et suivre leur état ?
Ici j'ai ajouté une action (update) qui va s'appliquer au niveau d'une entité.
Il m'a suffi de lier la source de l'action avec l'appel API à effectuer.
Ces actions peuvent être appliquées en parallèle sur différentes entités.
Côté template, il est très facile d'afficher l'état de chargement (loaded, loading, error) pour chaque action appliqué à l'entité.
Un autre exemple avec 2 actions, une action d'update et une de delete:
Comment ajouter des actions de masse qui s'appliquent sur les entités sélectionnées ?
Comme pour les actions précédentes, il suffit de lié une source qui émet une liste d'entités à un appel API et le tour est joué.
Ici, j'ajoute une action pour supprimer en masse et une autre pour mettre à jour en masse.
On peut voir l'état de chargement au niveau de chaque entité sélectionné.
Ajouter des sélecteurs (états dérivés)
On peut ajouter des sélecteurs au niveau d'une entité, mais aussi au niveau de store. Tout en profitant de l'autocomplétion et de la type-safety.
Ce sont des états dérivés qui sont calculés à chaque fois que le store émet une nouvelle valeur.
Je vais sans doute améliorer cette partie-là encore, en permettant de récupérer les sélecteurs provenant des entités, au sélecteurs du store.
Aller plus loin dans la personnalisation avec les reducer personnalisés
Je ne me suis pas arrêté là.
Ca peut-être utile de modifier une propriété de l'entité, que ce soit au chargement d'une action, à la fin de l'action suite à un succès ou une erreur.
Pour cela, il est possible d'utiliser des reducers personnalisées pour chaque action.
Dans l'exemple qui suit, lors d'une création d'une entité, soit, je l'ajoute à la liste si on est sur la première page soit, je la laisse dans la liste des outOfContextEntities
.
Sans rentrer trop dans les détails, cette liste permet de tracker les entités qui ont eu des actions appliquées. Je ne sais pas encore si je vais la garder ou l'exposer.
Aller toujours plus loin avec les delayedReducer
Inspiré par ce que propose l'opérateur groupBy de RxJs (duration
), j'ai ajouté des reducers qui s'appliqueront "en décalé", toujours trigger par une source.
Dans l'exemple qui suit, j'ai ajouté une action bulkdDelete
quand les entités sont supprimées, elles restent affichées avec l'état "Bulk deleted".
J'ai jouté un delayedReducer
pour les enlever de la liste après 15s.
J'ai aussi ajouté un reducer
pour ajouter une proprité ui.disappearIn$
à mon entité qui est un observable qui émet une string. Il va servir à afficher la phrase Remaining time before disappear: Xs
et se mettre à jour chaque seconde.
C'est poussé par les cheveux ? Oui sans doute, mais ça m'amusait.
A noter que si l'utilisateur change de page, elles entités supprimées sont retirées.
Voilà, je pense avoir fait le tour de ce que j'ai pu mettre en place.
Comme je l'ai dit, il faut maintenant tester ce système sur le terrain pour l'ajuster au mieux.
Le code final de l'exemple
Quelques points d'améliorations
Le nommage, je vais pour me rapprocher du nommage utilisé par TanStackQuery, pour plus d'harmonie.
J'ai rencontré beaucoup de limitations TS en termes de typage et de contraintes de typage. J'ai trouvé une solution proche de ce que je souhaitais, mais qui a quelques limitations agaçantes.
Bien que je n'ai pas eu besoin de déclarer de type à mon store, cela limite la façon dont je peux le moduler.
Je vais sans doute ajouter des fonctions intermédiaires avec des types parameters explicites pour améliorer la DX et certains cas de type-safety.
D'ailleurs, je vais essayer de faire un retour d'expérience avec les limitations de TS que j'ai rencontré lors du typage de ma fonction et les potentielles solutions.
La pagination (la source qui trigger l'appel api des entités) est très basique ici, je pense qu'elle n'est pas tout à fait adaptée. Parfois, l'appel API renvoie des informations utile sur la pagination.
Je vais exposer des events qui se passent dans le store pour permettre de réagir dessus. Ces events pourront aussi être utilisés comme source des actions. Ca pourra être utile pour faire un refresh d'une entité en cas d'erreur.
Clean le code de l'outil, je pensais partir sur un petit fichier pour géré le store, mais avec les imports et les types, je suis autour des 1000 lignes de code. C'est pas fou niveau lisibilité. Mais le code en lui-même reste assez simple à suivre. Il faudra que j'ajoute aussi les testes.
Ajouter des fonctionnalités. Une fois sur le terrain, je verrai ce qui pourrait me simplifier encore la vie et ajouter de nouvelles fonctionnalités. Par exemple, je pense rajouter des sélecteurs par défaut.
Son principal défaut, c'est que le code peut faire un peu grand si on ajoute plusieurs actions et reducers. L'ajout de fonction intermédiaire pourrait régler ce problème.
Pourquoi pas permettre à cet outil d'être utilisé de façon impérative.
Mieux gérer les cas d'erreur, comme les entités sont mise à jour de façon optimiste, il faudrait que je trouve un moyen pour remettre à leur valeur précédente en cas d'erreur.
Conclusion
Bien que le code ne soit pas encore clean, si tu veux voir comment c'est fait, il est dispo ici.
Si cet outil de server state management t'intéresse, fais le moi savoir en commentaire, ou en mettant un like sur ce poste. Ou encore en me donnant ton avis.
Je partagerai très prochainement le code, une fois que je l'aurai organisé.
Pour suivre l'avancement de ce projet n'hésite pas à me suivre sur linkedin Romain Geffrault.
A bientôt.