Cours Angular · Module 10: Gestion d'état
Sélecteurs et Effects - fenêtres et ponts dans NgRx
Dans cette leçon2
Dans NgRx, tout l'état de l'application repose dans un seul grand magasin, le store. Deux questions surgissent aussitôt : comment lire exactement la portion qui t'intéresse, et comment relier ce store au monde extérieur, c'est-à-dire au serveur ? Les deux outils de cette leçon y répondent : le sélecteur est une fenêtre ouverte sur une tranche de l'état, et l'Effect est le pont entre une action et une requête HTTP. Commençons par la lecture.
Le sélecteur - une fenêtre sur une tranche de l'état
Un sélecteur est une fonction qui extrait un morceau du store. Il possède un super-pouvoir, la mémoïsation : il ne se recalcule que lorsque ses dépendances changent - le reste du temps, il renvoie simplement le résultat déjà mémorisé.
1import { createFeatureSelector, createSelector } from '@ngrx/store';
2
3export const selectSamuraiState = createFeatureSelector<SamuraiState>('samurai');
4
5export const selectAllSamurai = createSelector(
6 selectSamuraiState,
7 state => state.samurai
8);
9
10export const selectSamuraiCount = createSelector(
11 selectAllSamurai,
12 samurai => samurai.length
13);Observe la mécanique de createSelector : les premiers arguments sont les sélecteurs d'entrée, le dernier est la fonction de projection qui combine leurs résultats. Quant à createFeatureSelector<SamuraiState>('samurai'), il sélectionne une branche entière de l'état - la feature - par son nom ; il ne crée aucun effet de bord, ne définit aucune action de chargement et n'enregistre aucun reducer dans le module. Remarque ensuite comment les sélecteurs se composent : selectAllSamurai lit la liste dans l'état, et selectSamuraiCount s'appuie sur lui pour en compter la longueur. C'est l'esprit de computed avec les signals - selectSamuraiCount ne se recalcule que si la liste change, pas à chaque autre modification du store. La vue ne se rafraîchit donc jamais pour rien.
Ne te méprends pas pour autant sur ce que la mémoïsation apporte : elle ne sauvegarde rien dans localStorage, elle ne trie pas automatiquement les résultats et elle ne valide aucun type à l'exécution. Elle évite un recalcul inutile, rien de plus. Quand tu as besoin d'un sélecteur paramétré, par id par exemple, tu renvoies un sélecteur depuis une fonction :
1export const selectSamuraiById = (id: number) => createSelector(
2 selectAllSamurai,
3 samurai => samurai.find(s => s.id === id)
4);
5
6export const selectSamuraiByClan = (clan: string) => createSelector(
7 selectAllSamurai,
8 samurai => samurai.filter(s => s.clan === clan)
9);Le schéma est exactement celui des validateurs personnalisés : la fonction extérieure reçoit le paramètre (id, clan) et renvoie un sélecteur prêt à l'emploi, avec selectAllSamurai comme sélecteur d'entrée et le find ou le filter comme fonction de projection. À partir d'une seule source, tu construis ainsi toutes les vues dont tu as besoin - un samouraï précis, un clan entier - sans jamais dupliquer la logique. Dans le composant, tu lis cela avec store.select(selectSamuraiByClan('Iga')). Le même schéma sert pour les drapeaux d'état : un selectLoading qui lit state.loading s'écrit mot pour mot comme selectAllSamurai.
L'Effect - un pont entre l'action et le serveur
Le sélecteur ne fait que lire. Mais d'où viennent les données déposées dans le store ? C'est là qu'entre en scène l'Effect : il écoute une action (par exemple "charger les samouraïs"), gère les effets de bord comme les requêtes HTTP, puis renvoie une nouvelle action porteuse du résultat. Il ne sert donc ni aux animations d'interface, ni au style des composants, ni à la validation de formulaire. Un Effect vit dans une classe @Injectable() qui récupère inject(Actions) et inject(HttpClient) :
1loadSamurai$ = createEffect(() =>
2 this.actions$.pipe(
3 ofType(loadSamurai), // ecoute cette action
4 switchMap(() =>
5 this.http.get<Samurai[]>('/api/samurai').pipe(
6 map(samurai => loadSamuraiSuccess({ samurai })), // succes -> action
7 catchError(error => of(loadSamuraiFailure({ error: error.message })))
8 )
9 )
10 )
11);Suis le pont étape par étape. Tout part de this.actions$, puis ., puis pipe, puis (ofType(loadSamurai)) : ofType(loadSamurai) écoute l'action et ne laisse passer qu'elle. Ensuite switchMap(() => http.get(...)) appelle l'API, map(samurai => loadSamuraiSuccess(...)) produit l'action de succès, et catchError(... => of(loadSamuraiFailure(...))) produit l'action d'erreur. Voilà le fondement de NgRx : un Effect ne modifie jamais le store directement, il émet une action que le reducer traduira en nouvel état. L'erreur elle-même est une action, pas une exception - d'où le of(...Failure) renvoyé par catchError.
Le choix de l'opérateur, lui, n'a rien d'anodin. Compare avec la version dédiée à l'ajout :
1addSamurai$ = createEffect(() =>
2 this.actions$.pipe(
3 ofType(addSamurai),
4 exhaustMap(({ samurai }) => // ignore les suivants tant que ca dure
5 this.http.post<Samurai>('/api/samurai', samurai).pipe(
6 map(saved => addSamuraiSuccess({ samurai: saved })),
7 catchError(error => of(addSamuraiFailure({ error: error.message })))
8 )
9 )
10 )
11);Toute la différence tient à switchMap face à exhaustMap. Au chargement, switchMap annule la requête précédente au profit de la nouvelle : seul le résultat le plus récent compte. À l'enregistrement, exhaustMap fait l'inverse, il ignore les déclenchements suivants tant que le précédent est en cours - ainsi un double clic sur "Enregistrer" ne crée pas deux samouraïs. Retiens cette paire comme un réflexe : switchMap pour lire, exhaustMap pour écrire.
Garde donc en mémoire les deux rôles de cette leçon : le sélecteur est la fenêtre par laquelle tu lis une tranche de l'état, et qui se recalcule toute seule au bon moment ; l'Effect est le pont qui transforme une action en action sur le monde, puis en nouvelle action. Le store reste pur, et toute la conversation avec le serveur passe par les Effects.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel est le principal avantage de createSelector() dans NgRx ?
2. À quoi servent les Effects dans NgRx ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète les sélecteurs pour filtrer les samouraïs par clan.
- Tri horizontal
Ordonne l'appel createSelector avec un sélecteur d'entrée et une fonction de projection (une seule ligne, de gauche à droite) :
- Tri vertical
Ordonne les étapes à l'intérieur de l'Effect loadSamurai$ (de l'écoute de l'action jusqu'au résultat) :
- Clique dans l'ordre
Clique sur les éléments dans l'ordre pour construire le début du flux dans un Effect qui écoute une action :