Les fondamentaux de NgRx, avec des exemples de code

Quand j’ai commencé à travailler avec NgRx, j’ai été confronté à une courbe d’apprentissage abrupte. Il a fallu pas mal de lecture et de recherche pour comprendre les éléments de base de NgRx. L’objectif premier de cet article est de transmettre les connaissances que j’ai acquises aux lecteurs afin que leur vie avec NgRx soit plus facile.
J’ai l’intention de couvrir les sujets suivants:

  • Qu’est-ce que NgRx ?
  • Éléments fondamentaux de NgRx: Store, Actions, Réducteurs, Sélecteurs, Effets
  • Interaction entre les composants NgRx
  • Avantages et inconvénients de NgRx

What Is NgRx?

Pour commencer, NgRx signifie Angular Reactive Extensions. NgRx est un système de gestion d’état basé sur le modèle Redux. Avant d’aller plus loin dans les détails, essayons de comprendre le concept d’état dans une application angular.

State

Théoriquement, l’état de l’application est la mémoire entière de l’application. En termes simples, l’état de l’application est composé de données reçues par les appels d’API, les entrées utilisateur, l’état de l’interface de présentation, les préférences de l’application, etc. Un exemple simple et concret d’un état d’application serait une liste de clients gérée dans une application CRM.

Essayons de comprendre l’état de l’application dans le contexte d’une application angular. Comme vous le savez, une application Angular est généralement composée de nombreux composants. Chacun de ces composants a son propre état et n’a aucune conscience de l’état des autres composants. Afin de partager des informations entre les composants parent-enfant, nous utilisons les décorateurs @Input et @Output. Cependant, cette approche n’est pratique que si votre application se compose de quelques composants, comme illustré ci-dessous.

Lorsque le nombre de composants augmente, il devient un cauchemar de transmettre les informations entre les composants uniquement via les décorateurs @Input et @Output. Prenons la figure suivante pour élaborer à ce sujet.

Si vous devez passer des informations du composant trois au composant six, vous devrez sauter quatre fois et impliquer trois autres composants. Comme vous pouvez le voir, c’est une façon très lourde et sujette aux erreurs de gérer l’état. C’est là que le motif Redux entre en jeu.

Redux

Redux est un modèle utilisé pour simplifier le processus de gestion des états dans les applications JavaScript (pas uniquement pour Angular). Redux est principalement basé sur trois grands principes.

  • Single source of truth / Source unique de vérité
  • Read-only state / État en lecture seule
  • State is modified with pure functions / L’état est modifié avec des fonctions pures

Single source of truth : Cela signifie que l’état de votre application est stocké dans une arborescence d’objets dans un seul magasin. Le magasin est responsable du stockage des données et de la fourniture des données aux composants chaque fois que cela est demandé. (Je fais référence aux applications Angular ici. Mais Redux peut être appliqué à n’importe quelle application JavaScript en général.) Selon cette architecture, les données circulent entre le magasin et les composants, plutôt que d’un composant à un autre. La figure suivante illustre ce concept.

Read-only state – En d’autres termes, l’état est immuable. Cela ne signifie pas nécessairement que l’état est toujours constant et ne peut pas être modifié. Cela implique seulement que vous n’êtes pas autorisé à changer d’état directement. Afin d’apporter des modifications à l’état, vous devez envoyer des actions (dont nous parlerons en détail plus tard) de différentes parties de votre application vers le magasin.

State is modified with pure functions – Les actions de répartition déclenchent un ensemble de fonctions pures appelées réducteurs. Les réducteurs sont chargés de modifier l’état de différentes manières en fonction de l’action reçue. Une chose clé à noter ici est qu’un réducteur renvoie toujours un nouvel objet d’état avec les modifications.

NgRx

NgRx est un groupe de bibliothèques inspiré du modèle Redux. Comme son nom l’indique, NgRx est écrit spécifiquement pour les applications Angular en tant que solution de gestion d’état. Nous plongerons dans les blocs de construction fondamentaux de la bibliothèque NgRx dans la section suivante. Veuillez noter que j’utiliserai NgRx version 8 pour tous les exemples de codes.

Éléments fondamentaux de NgRx : Store, Actions, Reducers, Selectors, Effects

The store : Le magasin est l’élément clé de tout le processus de gestion des états. Il détient l’état et facilite l’interaction entre les composants et l’état. Vous pouvez obtenir une référence au magasin via l’injection de dépendance angular, comme indiqué ci-dessous.

constructor(private store: Store<AppState>) {}

Cette référence de magasin peut être utilisée par la suite pour deux opérations principales:

  • Pour envoyer des actions au magasin via la méthode store.dispatch(…), qui à son tour déclenchera des réducteurs et des effets
  • Pour récupérer l’état de l’application via les sélecteurs

Structure d’une arborescence d’objets d’état

Supposons que votre application se compose de deux modules de fonctionnalités appelés Utilisateur et Produit. Chacun de ces modules gère différentes parties de l’état général. Les informations sur les produits seront toujours conservées dans la section des produits de l’état. Les informations utilisateur seront toujours conservées dans la section utilisateur de l’état. Ces sections sont également appelées tranches (slices) .

Actions

Une action est une instruction que vous envoyez au magasin, éventuellement avec des métadonnées (payload). En fonction du type d’action, le magasin décide des opérations à exécuter. Dans le code, une action est représentée par un vieil objet JavaScript simple avec deux attributs principaux, à savoir le “type” et “payload“. Payload est un attribut facultatif qui sera utilisé par les réducteurs pour modifier l’état. L’extrait de code et la figure suivants illustrent ce concept.

{
  "type": "Login Action",
  "payload": {
    userProfile: user
  }
}

NgRx version 8 fournit une fonction utilitaire appelée createAction pour définir les créateurs d’actions (pas les actions, mais les créateurs d’actions). Voici un exemple de code pour cela.

export const login = createAction(
    "[Login Page] User Login",
    props<{user: User}>()
);

Vous pouvez ensuite utiliser le créateur d’action login (qui est une fonction) pour créer des actions et les envoyer au magasin “Store” comme indiqué ci-dessous. user est l’objet Payload que vous passez dans l’action.

this.store.dispatch(login({user}));

Reducers

Les réducteurs sont responsables de la modification de l’état et du renvoi d’un nouvel objet d’état avec les modifications. Les réducteurs prennent en compte deux paramètres, l’état actuel et l’action. En fonction du type d’action reçu, les réducteurs effectueront certaines modifications de l’état actuel et produiront un nouvel état. Ce concept est présenté dans le diagramme ci-dessous.

Semblable aux actions, NgRx fournit une fonction utilitaire appelée createReducer pour créer des réducteurs. Un appel de fonction createReducer typique aimerait ce qui suit.

export const initialAuthState: AuthState = {
    user: undefined
};

export const authReducer = createReducer(

    initialAuthState,

    on(AuthActions.login, (state, action) => {
        return {
            user: action.user
        }
    }),

    on(AuthActions.logout, (state, action) => {
        return {
            user: undefined
        }
    })

);

Comme vous pouvez le voir, il prend l’état initial (l’état au démarrage de l’application) et les fonctions de changement d’état un-à-plusieurs qui définissent comment réagir aux différentes actions. Chacune de ces fonctions de changement d’état reçoit l’état actuel et l’action en tant que paramètres, et renvoie un nouvel état.

Effects

Les effets vous permettent d’effectuer des effets secondaires lorsqu’une action est envoyée au magasin “Store“. Essayons de comprendre cela à travers un exemple. Lorsqu’un utilisateur se connecte avec succès à une application, une action de type Login Action est envoyée au magasin avec les informations utilisateur dans le Payload. Une fonction de réduction écoutera cette action et modifiera l’état avec les informations utilisateur. En outre, vous souhaitez également enregistrer les informations utilisateur dans le stockage local du navigateur. Un effet peut être utilisé pour effectuer cette tâche supplémentaire (effet secondaire / side effect).
Il existe plusieurs façons de créer des effets dans NgRx. Voici une manière brute et explicite de créer des effets. Veuillez noter que vous n’utilisez généralement pas cette méthode pour créer des effets. Je n’ai pris cela qu’à titre d’exemple pour expliquer ce qui se passe derrière le rideau.

@Injectable()
export class AuthEffects {

    constructor(private actions$: Actions, private router: Router) {

      const login$ = this.actions$
                      .pipe(
                        ofType(AuthActions.login),
                        tap(action => localStorage.setItem('user',
                                JSON.stringify(action.user))
                        )
                      );

      login$.subscribe();

    }
}
  • actions$ observable émettra des actions reçues par le magasin. Ces valeurs passeront par une chaîne d’opérateurs.
  • ofType est le premier opérateur utilisé. Il s’agit d’un opérateur spécial fourni par NgRx (pas RxJS) pour filtrer les actions en fonction de leur type. Dans ce cas, seules les actions de type login seront autorisées à traverser le reste de la chaîne d’opérateurs.
  • tap est le deuxième opérateur utilisé dans la chaîne pour stocker les informations utilisateur dans le stockage local du navigateur. le Tap est généralement utilisé pour effectuer des effets secondaires dans une chaîne d’opérateurs.
  • Finaly, nous devons nous abonner manuellement au login$ observable.

Cependant, cette approche présente quelques inconvénients majeurs.

  • Vous devez vous abonner manuellement à l’observable, ce qui n’est pas une bonne pratique. De cette façon, vous devrez toujours vous désinscrire manuellement, ce qui entraîne un manque de maintenabilité.
  • Si une erreur apparaît dans la chaîne d’opérateurs, l’observable sortira en erreur et arrêtera d’émettre les valeurs suivantes (actions). En conséquence, l’effet secondaire ne sera pas effectué. Par conséquent, vous devez avoir un mécanisme en place pour créer manuellement une nouvelle instance observable et vous réabonner si une erreur se produit.

Afin de surmonter ces problèmes, NgRx fournit une fonction utilitaire appelée createEffect pour créer des effets. Un appel de fonction createEffect typique ressemblerait à ce qui suit.

    login$ = createEffect(() =>
        this.actions$
            .pipe(
                ofType(AuthActions.login),
                tap(action => localStorage.setItem('user',
                        JSON.stringify(action.user))
                )
            )
    ,
    {dispatch: false});

La méthode createEffect prend une fonction qui retourne un objet observable et (facultativement) un objet de configuration en tant que paramètres.
NgRx gère l’abonnement à l’observable renvoyé par la fonction de support, et par conséquent, vous n’avez pas à vous abonner ou à vous désabonner manuellement. De plus, si une erreur se produit dans la chaîne d’opérateurs, NgRx créera une nouvelle observable et se réabonnera pour s’assurer que l’effet secondaire est toujours exécuté.

Si dispatch est true (valeur par défaut) dans l’objet de configuration, la méthode createEffect renvoie une Observable<Action>. Sinon, il renvoie un Observable<Unknonw>. Si la propriété dispatch est true, NgRx s’abonnera à l’observable retourné de type Observable <Action> et distribuera les actions reçues au magasin “Store“.

Si vous ne mappez pas l’action reçue à un autre type d’action dans la chaîne d’opérateurs, vous devrez définir la distribution sur false. Sinon, l’exécution se traduira par une boucle infinie, car la même action sera distribuée et reçue dans les $actions stream encore et encore. Par exemple, vous n’êtes pas obligé de définir la dispatch sur false dans le code ci-dessous, car vous mappez l’action d’origine à un autre type d’action dans la chaîne d’opérateurs.

loadCourses$ = createEffect(
    () => this.actions$
        .pipe(
            ofType(CourseActions.loadAllCourses),
            concatMap(action =>
                this.coursesHttpService.findAllCourses()),
            map(courses => allCoursesLoaded({courses}))

        )
);

Dans le scénario ci-dessus :

  • L’effet reçoit des actions de type loadAllCourses.
  • Une API est appelée et les cours sont chargés comme effet secondaire.
  • La réponse de l’API à une action de type allCoursesLoaded est mappée et les cours chargés sont transmis en tant que Payload à l’action.
  • Enfin, l’action allCoursesLoaded qui a été créée est envoyée au magasin. Ceci est fait par NgRx sous le capot.
  • Un réducteur écoutera l’action allCoursesLoaded entrante et modifiera l’état avec les cours chargés.

Selectors

Les sélecteurs sont de pures fonctions utilisées pour obtenir des tranches de l’état du magasin. Comme indiqué ci-dessous, vous pouvez interroger l’état même sans utiliser de sélecteurs. Mais cette approche présente encore une fois quelques inconvénients majeurs.

const isLoggedIn$ = this.store.pipe(
map(state => !!state.user)
);
  • store est un observable auquel vous pouvez vous abonner. Chaque fois que le magasin reçoit une action, le magasin transmet l’objet d’état à ses abonnés.
  • Vous pouvez utiliser des fonctions de mappage pour obtenir des tranches d’état et effectuer tout calcul si nécessaire. Dans l’exemple ci-dessus, nous obtenons la tranche User dans l’arborescence des objets d’état et la convertissons en un booléen pour déterminer si l’utilisateur s’est connecté ou non.
  • Vous pouvez vous abonner manuellement à l’observable isLoggedIn$ ou l’utiliser dans un modèle angular avec un tube asynchrone pour lire les valeurs émises.

Cependant, cette approche présente un inconvénient majeur. En général, le magasin reçoit fréquemment des actions de différentes parties de l’application. Conformément à l’implémentation ci-dessus, chaque fois que le magasin reçoit une action, un objet d’état sera émis par le magasin. Et cet objet d’état passera à nouveau par la fonction de mappage et mettra à jour l’interface utilisateur.

Toutefois, si le résultat de la fonction de mappage n’a pas changé depuis la dernière fois, il n’est pas nécessaire de mettre à nouveau l’interface à jour. Par exemple, si le résultat de map(state => !!state.user) n’a pas changé depuis la dernière exécution, nous n’avons pas à pousser à nouveau le résultat vers l’interface utilisateur / l’abonné. Pour y parvenir, NgRx (et non RxJS) a introduit un opérateur spécial appelé select. Avec l’opérateur de sélection, le code ci-dessus changera comme suit.

const isLoggedIn$ = this.store.pipe(
select(state => !!state.user)
);

L’opérateur de sélection empêchera les valeurs d’être transmises à l’interface utilisateur / aux abonnés si le résultat de la fonction de mappage n’a pas changé depuis la dernière fois.
Cette approche peut être encore améliorée. Même si l’opérateur de sélection ne transmet pas les valeurs inchangées à l’interface utilisateur / aux abonnés, il doit tout de même prendre l’objet state et effectuer le calcul pour obtenir le résultat à chaque fois.
Comme déjà expliqué ci-dessus, un état sera émis par l’observable lorsque le magasin recevra une action de l’application. Une action ne met pas toujours à jour l’état. Si l’état n’a pas changé, le résultat du calcul de la fonction de mappage ne changera pas non plus. Par conséquent, nous n’avons pas à refaire le calcul si l’objet d’état émis n’a pas changé depuis la dernière fois. C’est là que les sélecteurs entrent en jeu.
Un sélecteur est une fonction pure qui conserve une mémoire des exécutions précédentes. Tant que l’entrée n’a pas changé, la sortie ne sera pas recalculée. Au lieu de cela, la sortie sera renvoyée de la mémoire. Ce processus s’appelle la mémorisation.
NgRx fournit une fonction utilitaire appelée createSelector pour créer des sélecteurs avec une capacité de mémorisation. Voici un exemple de la fonction utilitaire de création de sélecteur.

export const isLoggedIn = createSelector(
    state => state['auth'],
    auth =>  !!auth.user
);

La fonction createSelector utilise des fonctions de mappage un-à-plusieurs qui donnent différentes tranches de l’état et une fonction de projecteur qui effectue le calcul. La fonction de projecteur ne sera pas appelée si les tranches d’état n’ont pas changé depuis la dernière exécution. Pour utiliser la fonction de sélection créée, vous devez la passer comme argument à l’opérateur de sélection.

this.isLoggedIn$ = this.store
  .pipe(
    select(isLoggedIn)
  );

Interaction entre les composants NgRx

La figure suivante illustre comment les différents composants de l’écosystème NgRx interagissent les uns avec les autres.

Avantages et inconvénients de NgRx


Avantages

Le concept d’une source unique de la vérité permet aux composants de partager plus facilement des informations dans une application angular.
L’état de l’application ne peut pas être modifié directement par les composants. Seuls les réducteurs sont capables de changer d’état. Cela facilite le débogage.


Les inconvénients
Il y a une courbe d’apprentissage abrupte lorsque vous commencez à travailler avec NgRx.
L’application sera un peu verbeuse car vous devrez introduire plusieurs nouveaux artefacts, tels que des réducteurs, des sélecteurs, des effets, etc.

Conclusion


L’objectif principal de cette pièce était de vous donner une introduction aux concepts de NgRx. Je prévois de mettre en œuvre une application angular complète basée sur NgRx dans ma prochaine pièce.

Tagged:

Laisser un commentaire