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 !
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)))
)
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 withEntityLoader
sans 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)))
);
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;
}
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:
Voici comment j'ai créé simplement withBooksFilter1
grâce à withFeatureFactory
Si tu dois retenir qu'une chose, c'est bien l'utilisation de withFeatureFactory
:
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 :
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).
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
})
),
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é parfetchAllFilters
dans lefiltersMapper
.
La suite 👇
Cas 4: 🤯 Whoa une feature qui propose de l'autocomplete sans fonction intermédiaire !
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
:
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.
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:
- Facebook: Instinct AniMal - SOS Faune Sauvage
- Instagram: Instinct AniMal - SOS Faune Sauvage
- Faire un don
Top comments (0)