Bibliothèque Numérique ENI :
tous nos livres & vidéos, en accès illimité 24h/24. Cliquez ici
-100€ sur la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres & vidéos
  2. Angular
  3. Gestion d'état et architecture d'un projet
Extrait - Angular Développez vos applications web avec le framework JavaScript de Google (4e édition)
Extraits du livre
Angular Développez vos applications web avec le framework JavaScript de Google (4e édition) Revenir à la page d'achat du livre

Gestion d'état et architecture d'un projet

Introduction

Ce que l’on appelle l’état, c’est l’ensemble des variables d’une application, on peut parler d’état local, pour représenter les propriétés d’un composant qui seront déterminantes pour l’affichage d’un produit, ou d’un état global pour parler de données qui sont gardées en mémoire afin d’être récupérées par divers composants.

Dans ce chapitre, nous allons explorer avec précision le state management (la gestion d’état) dans une application. C’est un point des plus importants du développement d’applications, pour garantir la cohérence des données et permettre à une application de grandir sans être un fardeau pour certains développeurs, car la maintenance d’une application de grande taille, ou qui grossit rapidement, se complexifie tout autant au niveau des données.

Bonnes pratiques

1. Quelques principes

a. Principes de code

L’immutabilité : cela repose sur un principe simple : une donnée ne doit pas être modifiée après sa création. À la place, chaque mise à jour doit produire une nouvelle version de cette donnée. Cela est important pour la prédictibilité, nous savons qu’une fois une variable en main, ses propriétés ne vont pas changer, évitant de mauvaises surprises. Grâce à cela la détection de changements devient plus simple.

Exemple :

const user = { name:"Mathieu", favoriteTvShow: "Pingu" }; 
 
// Mutable way 
user.favoriteTvShow = "Game of Thrones" 
 
// Immutable way 
const newUser = { ...user, favoriteTvShow: "Game of Thrones" } 

Le concept de Single Source of Truth : il consiste à centraliser les données dans une source unique et fiable. Cette approche permet d’éviter les désynchronisations dues à des duplications et/ou des accès multiples non contrôlés aux données. Dans le chapitre Les services, nous avions mentionné une interface d’achat en ligne où plusieurs sections affichaient la même information. Si chacun de ces endroits possède une variable locale de l’information, cela engendre autant de risques que l’un ne se mette pas à jour à cause d’un oubli dans le code. En revanche, si chaque section va puiser la donnée sur une même source, le câblage devient tout de suite plus simple et fiable.

La notion de pureté : ce terme a déjà été utilisé dans le chapitre Les composants pour parler de pipe ou de fonction pure et impure. Pour rappel, le principe dit qu’une fonction, pour être pure, doit uniquement dépendre de ses paramètres et n’engendre aucun effet de bord, c’est-à-dire qu’elle n’essaie pas de consulter ou de modifier une information du scope parent, par exemple.

Un flux de données unidirectionnel : il s’agit, autant que possible, d’établir un sens unique à la donnée. On peut comparer cela au passage de données entre composants dans Angular. Le premier principe...

Définir sa propre gestion d’état

Nous allons explorer différentes façons d’organiser et de structurer notre gestion d’état. Nous commencerons par une approche libre, similaire à celle utilisée dans les exemples de code des chapitres précédents, en nous concentrant sur des pratiques qui garantissent la fiabilité et la maintenabilité de l’état sans imposer de cadre strict. Ensuite, nous évoluerons vers une implémentation manuelle d’un store suivant le pattern Redux, afin de mieux comprendre cette architecture.

Pour illustrer les différentes façons de gérer l’état dans une application, nous utiliserons un exemple commun : nous voulons afficher une page listant les utilisateurs, et au clic, être redirigés sur la page de détails de l’utilisateur. Puis en tant que base de départ : deux pages UserListComponent et UserDetailComponent, ainsi qu’un service dédié aux requêtes HTTP ApiService.

1. Solution simple

Avant de plonger dans des implémentations typées Redux, nous allons commencer par une approche simple et directe, en utilisant les services Angular pour gérer notre état. Un service va mettre à disposition des données et des méthodes pour la manipuler comme nous l’avions vu dans le chapitre Les services.

Nous pouvons donc créer notre service UserService qui sera garant des données liées. On exposera la liste des utilisateurs en lecture seule car c’est potentiellement une donnée réutilisable ; c’est en fonction des besoins que l’on va diriger notre code entre stocker les données ou les requêter à chaque fois. Puis une succession de méthodes :

  • getUserById : la récupération d’un utilisateur depuis l’API. Ce qui pourrait aussi se traduire par un objet Resource, dans le cas où nous voudrions stocker la dernière valeur.

  • getUserByIdWithComments : ici, on croise deux données, à savoir les utilisateurs et des commentaires, pour fournir un ensemble d’un utilisateur avec les commentaires dont il est l’auteur.

  • load : la demande de charger la liste des utilisateurs.

Implémentation

export class UserService { ...

Utilisation de la librairie SignalStore

Dans cette section, nous allons explorer NgRx SignalStore (https://ngrx.io/guide/signals), une extension récente de la bibliothèque NgRx qui combine les principes de Redux avec la simplicité des signaux d’Angular.

1. Création d’un store

Commencez par installer la librairie avec une commande de votre gestionnaire de package ou bien la suivante :

ng add @ngrx/signals@latest 

Pour créer un store, il suffit de définir le modèle de son state, puis d’appeler la méthode signalStore distribuée par la librairie. Cette méthode prendra en paramètre une série d’instructions appelées features qui permettent de configurer le store. A minima, une feature est obligatoire afin de créer un store. Voici un exemple avec l’appel à withState :

export type User = { 
  id: number; 
  firstName: string; 
  lastName: string; 
  isActive: boolean; 
  isDeleted: boolean; 
}; 
 
type UserState = { 
  userList: User[]; 
  isLoading: boolean; 
  error: string | null; 
 filters: { isActive: boolean; isDeleted: boolean }; 
}; 
 
const initialState: UserState = { 
  userList: [], 
  isLoading: false, 
  error: null, 
  filters: { isActive: true, isDeleted: false }, 
}; 
 
export const UserStore = signalStore(withState(initialState)); 

Pas de classe, juste du code fonctionnel simple.

2. Configuration du store

Pour configurer le store, on va donc jouer sur une combinaison de features afin d’ajouter tous les éléments comme nos sélecteurs, nos fonctions, et même des événements du cycle de vie du store comme :

  • withState : pour définir le state par défaut ;

  • withComputed : pour rajouter des sélecteurs en tant que signaux calculés ;

  • withMethods : pour nos méthodes ;

  • withHooks : pour réagir aux événements du cycle de vie.

La plupart des features prennent en paramètre un callback afin de passer en paramètre le store, et attendent en retour un objet.

a. L’accès aux données...

Autres librairies

Il existe d’autres bibliothèques qui répondent à des besoins spécifiques ou proposent des approches différentes. Selon la complexité de votre application et vos préférences en matière d’architecture, vous pourriez envisager des alternatives comme celles à suivre.

NgRx Store

NgRx Store implémente une version plus stricte du modèle Redux, plus proche de l’implémentation d’exemple de la section Redux, sous-section Définition de ce chapitre que des autres démonstrations. Par exemple, nous allons définir de véritables actions comme étant des messages accompagnés d’une donnée (payload). Ainsi, tout le flux est une série de messages historisés, proche (dans l’idée) d’un pattern "Event sourcing" où la donnée est vue comme une série de modifications plutôt que comme un état final uniquement.

Cependant, cette rigueur s’accompagne d’un code "boilerplate" plus conséquent, ce qui peut ralentir le développement initial. NgRx Store est donc particulièrement adapté aux projets complexes où l’évolutivité et la maintenabilité sont primordiales. 

Angular-Query

Adaptation de TanStack query pour Angular, cette bibliothèque est conçue...

Projet fil rouge

Nous allons réorganiser notre arborescence de fichiers et utiliser SignalStore dans notre application. Cela va demander plusieurs modifications structurelles dont voici les étapes :

  • appliquer la structure de fichiers proposée dans la sous-section Structure de fichiers de ce chapitre ;

  • scinder les services existants :

    • un store,

    • un service de requêtes HTTP,

  • mettre à jour les composants ;

  • vous pouvez commencer par installer la bibliothèque, puis appliquer la nouvelle arborescence sur les dossiers app/shared/, layout/, admin/, board/ et enfin identity/.

Une fois cela fait, nous allons créer nos deux stores, un pour le tableau et un second pour l’authentification.

1. Création de l’AuthStore

Commençons par la partie authentification de l’application.

Modifiez le service existant afin de le renommer et de le limiter aux requêtes :

export class AuthApiService { 
  private http = inject(HttpClient); 
 
  login(email: string, password: string) { 
    return this.http.post<AuthResponse>('/api/login', { 
      email, 
      password, 
    }); 
  } 
 
  logout() { 
    return this.http.post('/api/logout', {}); 
  } 
} 

Puis, nous partirons sur le state suivant :

type AuthState = { 
  user: User | null; 
  authToken: string | null; 
  isLoggingin: boolean; 
  error: string | null; 
}; 

 Veuillez créer le store avec :

  • le state défini ci-dessus ;

  • deux méthodes : login et logout qui gardent leur logique mais sont adaptées pour l’utilisation de rxMethod ;

  • une méthode simple pour remettre l’erreur à zéro ;

  • les mêmes sélecteurs qu’actuellement : isUserConnected, isUserAdmin et username ;

  • le jeton d’authentification courant du stockage, récupéré dans l’événement onInit afin de nourrir le state.

withHooks((store) => ({ 
  onInit() { 
    const authToken...

Conclusion

Cette fois, nous avons vu des principes fondamentaux de la gestion d’état et de l’architecture d’une application. D’abord, les bonnes pratiques pour structurer l’application, des principes pragmatiques, une organisation claire de fichiers et des approches à adopter. Ensuite, nous avons pu nous attarder sur les concepts dans Redux, évaluant son rôle et sa pertinence.

Nous avons également appris à concevoir plusieurs systèmes de gestion d’état, du simple vers du plus complexe, jusqu’à l’utilisation d’une bibliothèque, mentionnant plusieurs choix possibles.

Enfin, dans l’exercice pratique, nous avons intégré ces concepts à notre application Kanban en créant deux stores. Le code complet de cet exercice est disponible sur GitHub à l’adresse suivante : https://github.com/KevinAlbrecht/angular-eni/tree/chapitre-10. Les étapes d’implémentation ayant été plutôt lourdes en termes de changements sur le projet, comme toujours, il est conseillé de prendre le temps de tester, modifier, "jouer" avec le code pour mieux comprendre et apporter d’éventuelles améliorations qui semblent intéressantes, afin de vous approprier le sujet

Dans le chapitre Tester son application, nous verrons comment créer des tests, unitaires...