Librairies Ngrx Pour Une Application Angular Reactive Part 1 Ngrx Store
Mon article précédent traitait globalement de l’architecture proposée par Redux pour des web app dites “réactives”. Je vous recommande de le lire si vous n’êtes pas familier avec Redux : Le State management pour les webapps avec Redux.
Maintenant, sur le même sujet, nous allons nous concentrer sur les applications Angular et nous allons voir des librairies de la team ngrx. Cet article est divisé en 3 parties :
- ngrx/store: la brique principale représentant l’ensemble du store redux.
- ngrx/effects: une librairie de side-effect, pour nous permettre de communiquer avec une donnée externe ( ex: une API ).
- ngrx/router-store: tout simplement un pont entre notre router angular et le store redux.
ngrx/store
Notions
Qu’est ce que ngrx/store ?
Le ngrx/store est tout simplement une librairie fournissant les éléments (actions, dispatcher, reducer …etc.) pour une architecture “reactive” type state management, basée sur Redux. Elle fonctionne avec RxJS ( donc Observable based ), implémentée avec Typescript et donc tout bonnement adaptée pour Angular.
Github → https://github.com/ngrx/platform/tree/master/docs/store
Container components vs Presentational Components
Il faut bien dissocier 2 types de composants dans l’architecture Redux, la différence est très simpliste :
- Le Container Component fait partie du dataflow de Redux
- Récupère la donnée du Store
- Dispatch des actions
- Le Presentational Component n’a aucun lien avec le cycle de Redux ( les points ci-dessous étant spécifiques à Angular )
- Récupère la donnée par des binding @Input()
- Rappelle une action via un binding @Output()
exemple:
Mise en place
Introduction
⚐ Je passe sur certains détails Typescript, imports de modules …etc. Et je ne montre pas tout le découpage, mais tout ceci est visible via un lien vers le code complet à la fin de l’article, enjoy.
Pour l’exemple, nous allons créer un simple listing d’éléments, en l’occurence des films (pour info je pars d’une application générée par la cli angular, attention nous réutiliserons ce projet pour les parties 2 et 3 de cet article)
Dans l’ordre nous allons voir:
- Le data flow grâce au Store
- Les Actions
- Notre State
- Notre Reducer
- Les Selectors
- Un appel depuis un component
Création des Actions
Tout d’abord nous allons créer des actions liées aux différents états de la récupération d’un film. Pour rappel une action est simplement une association d’un type et d’un payload.
- Les films sont en chargement.
- Les films ont été chargés avec succés.
- Une erreur est survenue ( veuillez réessayer plus t… ) pendant le chargement des films.
|
|
Création du Reducer
Pour pouvoir créer notre Reducer j’ai besoin d’avoir un State, simple interface avec une propriété Data pour contenir la donnée ( en l’occurence un tableau de Movie ), un booléen pour signifier si nous sommes en chargement et une erreur. Nous verrons après comment rattacher ce modèle au Store.
|
|
Maintenant notre Reducer, en prenant soin de définir un State initial.
|
|
Plusieurs points à noter là-dessus. La fonction reducer prend en paramètre un state (avec l’initial state en propriété par défaut) et une action du type souhaité, on retrouve là notre caracteristique “pure”.
A noter aussi que nous prenons le state initial par défaut et que le Reducer modifie uniquement les propriétés nécéssaires, en l’occurence, lors de l’événement “GET_MOVIES” je spécifie à mon state qu’il est en chargement et donc le isLoading passe à true, lors d’un success ou d’un cas d’erreur, j’élimine cet etat de chargement et je nourris respectivement la data ou l’erreur avec le payload.
Définition du State
Il faut voir le State de notre application comme un arbre, il y aura differents niveaux et accesseurs suivant la donnée que l’on souhaite acquérir.
Dans l’app module il va falloir rajouter 2 imports:
|
|
Le premier définit un tableau de Reducers initiaux et le suivant permet d’importer des Reducers regroupés par “features” pour les modules lazy loadés. Dans l’exemple, ‘elements’ représente la clef de la feature, et le second paramètre est un objet qui représente les Reducers pour l’ensemble de notre feature. Voilà comment je l’ai défini :
|
|
Où cette fois-ci “reducer” correspond bien à notre fonction créée précédemment.
Création des Selectors
Pour pouvoir accèder à un élément de notre state, il faut créer des Selectors. Il y a 2 types de Selectors, les FeatureSelector et les simples Selector, vous l’aurez compris, le FeatureSelector nous permet de récupérer au top-level le State d’une feature et les autres, des grappes à l’interieur de la feature, voici un exemple :
|
|
Ok, jusqu’ici on a déjà bien représenté le data flow de notre Store, nous pouvons dispatcher notre premiere action depuis un component.
Appel depuis un composant
Container component
Pour bien distinguer la notion de container et presentational component, nous allons en créer un de chaque, voyons le container, côté Typescript :
|
|
Rien d’extraordinaire, on injecte notre store pour le type voulu, et on récupère un Observable de notre state grâce aux selectors ( que nous avons créés précédement ) avec la méthode .select. Il ne reste plus qu’à dispatch une action, en l’occurence de type GET_MOVIES. Pour rappel, notre observable movies$ sera donc résolu 2 fois car notre Reducer et notre middleware vont être notifiés de l’action, et ce dernier renvoyant une action succeed ou error.
La vue:
|
|
⚐ A noter l’utilisation du pipe “async”
- aucun soucis pour gérer une valeur par défaut en attendant la résolution
- on peut l’utiliser plusieurs fois pour le même observable ( une seule souscription est faite)
- l’Unsubscribe est géré tout seul comme un grand
- Utilisé dans un binding, il mettra bien à jour la référence à chaque résolution de l’observable
- ..
Presentational Component
Ici le composant Movies de présentation:
|
|
⚐ A noter la changeDetectionStrategy d’angular sur “OnPush”, en effet la détection de changement ne se fera uniquement sur modification de référence passée sur les @Input() du composant (ce qui vaut pour le binding d’un observable via le pipe async comme on le fait plus haut) ou sur les event Handler
⚐ Code complet (article parties 1 & 2) disponible ici : https://github.com/Bubbuls/ngrx-demo
Voila
Jusqu’ici le store est mis en place, nous sommes capables de dispatcher une action, et de récupérer un nouveau state en fonction, nous avons vu plusieurs points comme le pipe async pour résoudre les observables, la stratégie de détection OnPush pour rajouter un peu d’immutabilité dans nos composants.
Mais nous n’avons pas de données, je vous invite à passer à la partie 2 de cet article qui montrera comment appeler un service Angular ou tout autre élément externe grâce aux side-effects (volontaires), ça se trouve ici : Librairies NGRX pour une application Angular réactive. Part 2⁄3 : ngrx/Effects