DEV Community

Cover image for NgRx SignalStore : Ajouter une Feature personnalisée avec une DX exceptionnelle
Romain Geffrault
Romain Geffrault

Posted on

NgRx SignalStore : Ajouter une Feature personnalisée avec une DX exceptionnelle

J'ai passé du temps pour chercher un meilleur moyen d'exposer des signalStoreFeature qui nécessitent des info du store existant. Et j'ai une super solution pour toi !

Before/after applying custom signalStore feature pattern

Voici l'exemple donné par NgRx pour ajouter une feature au signalStore qui nécessitent d'appeler une méthode venant du store:

withFeature(
    // 👇 has full access to the store
    (store) => withEntityLoader((id) => firstValueFrom(store.load(id)))
  )
Enter fullscreen mode Exit fullscreen mode

Ca semble plutôt ok, mais je n'ai pas l'indentation que créé withFeature => withEntityLoader. On perd en lisibilité et le withFeature n'a pas vraiment d'intérêt, si ce n'est aider le dev à intégrer sa feature.

Mon objectif est de pouvoir simplifier le code pour que tu puisses directement écrire withEntityLoadersans withFeature (et j'ai réussi):

signalStore(
  withMethods((store) => ({
    load(id: number): Observable<Entity> {
      return of({ id, name: 'John' });
    },
  })),
  withEntityLoader((store) => firstValueFrom(store.load(id)))
);
Enter fullscreen mode Exit fullscreen mode

Avec un code toujours ✅ Typesafe.

Lien stackblitz/ gihtub disponible à la fin de l'article.

En bossant sur l'ajout d'un outil de server-state management dans le signalStore, j'ai beaucoup appris sur la façon dont fonctionne le typage et comment faire pour offrir la meilleure DX à l'utilisateur de notre feature.

J'ai exploré plusieurs façons pour que tu puisses toi aussi profiter ces mécanismes dans ton quotidien sans te prendre la tête à gérer le typage.

C'est la promesse que je te propose grâce au premier exemple que je te présente juste après. Mais j'ai aussi pensé à ceux qui souhaitent aller plus loin dans la démarche et offrir une expérience utilisateur digne de ce nom.

Je me suis rendu compte en créant mon outil de server-state management qu'il est parfois nécessaire de créer des config hautement typées, basées sur le contenu du store existant, qui simplifient la DX via des autocomplete par example ou encore des fonctions qui ne sont obligatoires que dans certains cas.

Pour y arriver, j'ai dû me battre pour comprendre les limites de typage de TypeScript et surtout comment les contourner tout en limitant les types que l'utilisateur doit renseigner à la main pour profiter au maximum de l'inférence (les types que TS peut deviner) de TS.

Je ne vais pas rentrer plus loin dans les détails du typage, mais si cela t'intéresse n'hésite pas à m'envoyer un message ou à laisser un commentaire.

Toutefois, si t'as déjà essayé ce genre de manœuvre sans y parvenir sache que moi aussi, j'ai galéré comme un fou, et que j'ai retenue cette leçon:

Pour ajouter une feature dans le signalStore, tu dois typer explicitement ce que retourne ta feature

Ca ne te parle peut-être pas, mais toutes les façons que je vais te présenter pour y parvenir suivent cette règle.

Exemple:

// 👇 increment result is inferred by TS it knows it will return a number
function increment(count: number) {
 return count + 1;
}

// increment result is explicitly defined (TS will just check if the returned type match the return type from the signature.
function increment(count: number): number 👈 {
 return count + 1;
}
Enter fullscreen mode Exit fullscreen mode

Cas 1 : 🎁 C'est cadeau ! T'as juste à copier coller pour que ça marche (merci withFeatureFactory, bientôt dans NgRx ?)

Je te lâche la bombe, j'étais tellement content d'avoir trouvé ce système, car il permet de réaliser facilement l'objectif !

Voici le résultat quand on intègre notre feature dans le signalStore:
NgRx withfeature using withFeatureFactory

Voici comment j'ai créé simplement withBooksFilter1 grâce à withFeatureFactory

NgRx withBooksFilter1 using withFeatureFactory

Si tu dois retenir qu'une chose, c'est bien l'utilisation de withFeatureFactory:

NgRx withFeatureFactory implemntation

C'est vraiment top, car il n'y a qu'à passer notre feature en input du withFeatureFactory et la magie du typage se répercute.

Ca ne se voit peut-être pas, mais tout est bien typé.

J'ai même rajouter des tests de typage pour m'en assurer :

NgRx withFeatureFactory Test

C'est tellement pratique que je pense que ça peut être un super complément au withFeature. Je pense faire une PR dès que j'ai le temps (ma première 😄).

Attention, toutefois il n'est possible de passer qu'un seul paramètre à ta feature, n'hésite pas à créer un objet pour contourner cette limitation.

Passons au second cas !

Cas 2: 🎛️ Te permet d'avoir plus de contrôle et de personnalisation sur la création de ta feature

Le résultat dans le signalStore est identique au cas précédent.

Toutefois, ici, je te propose une approche plus explicite te permettant de plus facilement faire évoluer ta feature.

J'ai créé plusieurs utilitaires pour que tu puisses l'utiliser sans risquer de tout casser facilement (Et mon dieu que le typage par en sucette pour un rien).

NgRx customizable signalStorefeature

Il suffit de garder le template et de remplacer filterBooksFeature par ta feature.

Attention, toutefois il n'est possible de passer qu'un seul paramètre à ta feature, n'hésite pas à créer un objet pour contourner cette limitation.

Aller en va rentrer dans les cas de grand malade qui profitent à fond du typage TypeScript !

Cas 3: 🚀 Pour une solution hautement personnalisable, tes collègues seront bouche bée

Effet whoa assuré, cette technique nécessite des connaissances en typage pour pouvoir la personnalisé.

Comme tu le vois, l'utilisateur a accès au store (même si pas 100% utile dans notre cas), mais a surtout l'avantage de profiter de l'autocompéltion spécifique pour indiquer sur quelles listes de livres appliquer la feature de filtre.

Ce pattern est indispensable dans les configurations plus élaborées où l'on souhaite récupérer le type d'une output de notre configuration et la réutiliser dans cette même config.

Voici un exemple fictif ce que je raconte:

withBooksFilter3((store) =>
        booksSelector({
          booksPath: 'entities',
          fetchAllFilters: resource({
            loader: () => apiService.allFilters(),
          }),
          //👇 this mapper is only needed if fetchedFilters are not the same type as filters from our feature
          filtersMapper: (fetchedFilters) => toFeatureFilters(fetchedFilters),
          //              👆 the type of fetchedFilters is infer from the fetchAllFilters above
        })
      ),
Enter fullscreen mode Exit fullscreen mode

Depuis une fonction parente, s'il y a des fonctions retournées par la réponse, on ne peut pas propager le typage de ces sous fonctions à d'autres champs de la réponse. TypeScript perd le fil. Pour palier à ça, il faut utiliser une fonction intermédiaire. Comme ici avec booksSelector qui sert d'intermédiaire et permet de réutiliser le type retourné par fetchAllFilters dans le filtersMapper.

La suite 👇

Cas 4: 🤯 Whoa une feature qui propose de l'autocomplete sans fonction intermédiaire !

NgRx advanced signalStore feature result

Ici, nous n'avons pas la contrainte mentionnée précédemment donc on peut se permettre de ne pas ajouter de fonction intermédiaire.

Voici l'implémentation de withBooksFilter4:

NgRx advanced signalStore feature implementation

C'est un variant que je voulais garder de côté, car j'aime aussi cette approche qui reste malgré tout "simple", où il est très facile de personnalisé la config demandée à l'utilisateur.

🔗 Lien vers le code ! Stackblitz et Github

Pour tester et voir par toi même, voici le stackblitz

Ce n'est pas l'app Angular qui se lance, mais la suite de tests qui vont bien.

Le repo github ici.

Conclusion

J'ai expérimenté beaucoup d'approche pour trouver différentes façon pour parvenir à l'objectif. Je n'ai peut-être pas tout trouvé ! J'espère voir d'autres façon de faire !

En attendant, j'espère voir davantage de feature utilitaire comme withLinkedState/withResources... c'est plutôt simple maintenant à créer avec les techniques que je propose ici.

Pourquoi pas withServices (qui pourrait permettre de définir explicitement les services, withActionSource (l'équivalent des méthodes, mais pour profiter de la programmation déclarative/réactive).

On pourra voir des stores vraiment très explicites ce qui est une bonne chose !

En attendant, j'espère que ces patterns t'ont plu. N'hésite pas à mettre un petit commentaire pour me remercier ou à laisser une j'aime (ça m'aide et ça me fait plaisir pour continuer à proposer du contenu 👍).

Si tu ne me connais pas, je suis Romain Geffrault, je partage régulièrement du contenu Angualr/TypeScript/RxJs/Signal. N'hésite pas à aller voir mes autres articles et à me suivre sur Linkedin Romain Geffrault.

🐣 Un petit mot pour la cause animale - Instinct AniMal - SOS Faune Sauvage

Si vous avez apprécié cet article et souhaitez faire un geste, je vous invite à découvrir et soutenir l'association Instinct AniMal - SOS Faune Sauvage, qui œuvre pour soigner les animaux en détresse.

Instinct AniMal - SOS Faune Sauvage

Je les aide de diverses façons, et chaque jour, je valide mes pas du WeWard.

La majorité des participants sont bénévoles qui font un travail de dingue sans être rémunérés, car sauver des animaux est une valeur forte pour eux ! Et j'en suis très admiratif !

Liens:

Top comments (0)