Gestion d'État avec NgRx (ou alternatives) : Architectures Réactives
Contexte du cours : Maîtriser Angular : Développement d'Applications Web Modernes et Robustes
Introduction à la Gestion d'État dans les Applications Angular
Dans le développement d'applications web modernes, et particulièrement avec des frameworks comme Angular, la gestion des données et de l'état de l'interface utilisateur (UI) devient rapidement un défi majeur. À mesure que les applications grandissent en taille et en complexité, il devient difficile de suivre les changements de données, de déboguer les problèmes et de maintenir une logique prévisible.
C'est ici qu'interviennent les architectures de gestion d'état réactives. Elles fournissent des modèles et des bibliothèques pour organiser le flux de données, garantir la prévisibilité des changements d'état et faciliter la collaboration et la maintenance du code. Ce chapitre explorera les concepts fondamentaux de la gestion d'état, se concentrera sur NgRx comme solution de référence pour Angular, et présentera des alternatives pertinentes.
Comprendre la Gestion d'État
Qu'est-ce que l'état d'une application ?
L'état d'une application peut être défini comme l'ensemble des données qui décrivent l'état actuel de l'application et qui peuvent changer au fil du temps. Ces données influencent directement ce que l'utilisateur voit et avec quoi il peut interagir.
Exemples d'état :
- Données utilisateur : l'utilisateur est-il connecté ? Quel est son profil ?
- Données de l'API : liste de produits, détails d'une commande, articles d'un blog.
- État de l'UI : un modal est-il ouvert ? Un formulaire est-il en cours de soumission ? Une section est-elle déployée ?
- Données de formulaire : valeurs des champs, état de validation.
Les défis de la gestion d'état sans structure
Sans une approche structurée, la gestion de l'état peut entraîner plusieurs problèmes :
- Diffusion de la complexité (Spaghetti Code) : L'état peut être réparti dans de nombreux composants et services, rendant difficile de savoir où une donnée est modifiée et par qui.
- "Prop Drilling" : Le fait de passer des données via des propriétés à travers de nombreux niveaux de composants enfants, même si ces composants intermédiaires n'ont pas besoin des données. Cela rend le code verbeux et difficile à refactoriser.
- Incohérences des données : Plusieurs composants peuvent détenir des copies de la même donnée et la modifier indépendamment, entraînant des états contradictoires.
- Débogage complexe : Il est très difficile de tracer l'origine d'un bug lié à l'état lorsque les changements se produisent de manière imprévisible.
- Conditions de course : Des opérations asynchrones (par exemple, des appels API) peuvent modifier l'état dans un ordre inattendu, conduisant à des comportements erronés.
Pourquoi une solution dédiée est nécessaire ?
Les bibliothèques de gestion d'état résolvent ces problèmes en fournissant un point centralisé pour tout l'état de l'application. Elles imposent une structure et un ensemble de règles qui garantissent :
- Prévisibilité : L'état ne peut être modifié que de manière explicite et traçable.
- Maintenabilité : Le code est plus facile à comprendre, à déboguer et à faire évoluer.
- Performance : En optimisant la mise à jour des composants qui dépendent de l'état.
- Testabilité : La logique de modification de l'état est isolée et facile à tester unitairement.
- Scalabilité : La gestion d'applications de grande envergure devient gérable.
Introduction aux Architectures Réactives
Les solutions de gestion d'état pour Angular s'appuient fortement sur la programmation réactive, notamment via la bibliothèque RxJS.
Qu'est-ce que la programmation réactive ?
La programmation réactive est un paradigme de programmation axé sur la propagation des changements et la gestion des flux de données asynchrones. Au lieu de faire des requêtes ponctuelles pour des données (pull-based), vous vous abonnez à des flux de données qui poussent de nouvelles valeurs au fil du temps (push-based).
- RxJS : Est la bibliothèque de facto pour la programmation réactive en JavaScript. Elle introduit les Observables, qui sont des flux de données sur lesquels on peut s'abonner pour recevoir des notifications lorsqu'une nouvelle valeur est émise, une erreur se produit ou le flux se termine.
- Opérateurs : RxJS fournit une multitude d'opérateurs pour transformer, filtrer, combiner et gérer ces flux d'événements.
Le pattern Redux (Flux) et ses principes
De nombreuses bibliothèques de gestion d'état, y compris NgRx, sont inspirées du pattern Redux (lui-même inspiré de l'architecture Flux de Facebook). Ses principes fondamentaux sont :
-
Source unique de vérité (Single Source of Truth) : L'état de l'application est stocké dans un seul objet (appelé le "store"). Cela facilite le débogage et la persistance.
-
L'état est en lecture seule (State is Read-Only) : La seule façon de modifier l'état est d'émettre une "action" qui décrit ce qui s'est passé. L'état est immutable, ce qui signifie qu'on ne le modifie jamais directement, on retourne toujours une nouvelle instance de l'état.
-
Les changements sont effectués par des fonctions pures (Changes are made with Pure Functions) : Pour modifier l'état, vous écrivez des "reducers", qui sont des fonctions pures. Elles prennent l'état actuel et une action en entrée, et retournent un nouvel état. Elles n'ont pas d'effets secondaires et ne dépendent pas de l'environnement extérieur.
-
Flux de données unidirectionnel (Unidirectional Data Flow) : C'est le cœur de l'architecture Redux. Le flux de données suit toujours le même chemin prévisible :
- Vue (UI) : L'utilisateur interagit avec l'application (clic, saisie).
- Action : Un événement est émis pour décrire ce qui s'est passé (ex:
utilisateurConnecte,produitAjouteAuPanier). - Dispatcher : L'action est "dispatchée" vers le store.
- Reducers : Les reducers écoutent les actions. Lorsqu'une action pertinente est reçue, ils calculent un nouvel état basé sur l'état précédent et l'action.
- Store : Le store met à jour son état avec le nouvel état retourné par les reducers.
- Sélectionneurs (Selectors) : Les composants s'abonnent aux parties pertinentes du store via des sélecteurs pour refléter les changements d'état dans l'UI.
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ Composant │─► │ Action │─► │ Dispatcher │─► │ Reducers │─► │ Store │ │ (UI) │ │ │ │ │ │ │ │ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ └───────────┘ ▲ │ └───────────────────────────────────────────────────────────────────────────┘ SelectorsDiagramme conceptuel du flux de données unidirectionnel.
Ce flux prévisible rend l'état de l'application très facile à suivre, à déboguer et à tester.
NgRx : L'Implémentation Redux pour Angular
NgRx est la bibliothèque de gestion d'état de facto pour Angular. Elle fournit un ensemble d'outils et de modules pour implémenter le pattern Redux dans des applications Angular, en exploitant pleinement RxJS.
Pourquoi NgRx ?
- Officiel et Intégré : C'est la solution la plus recommandée par la communauté Angular pour les applications complexes.
- Prévisibilité : Force un flux de données unidirectionnel et l'immutabilité de l'état.
- Débogage Puissant : Compatible avec les Redux DevTools, permettant de remonter le temps ("time-travel debugging"), d'inspecter chaque action et chaque changement d'état.
- Performance : Optimisé pour Angular, utilise la mémoïsation via les sélecteurs.
- Scalabilité et Testabilité : Facilite le développement d'applications à grande échelle et rend le code facile à tester unitairement.
Les briques fondamentales de NgRx
NgRx est composé de plusieurs modules, chacun ayant un rôle spécifique dans la gestion de l'état.
Store (@ngrx/store)
Le Store est le cœur de NgRx. C'est un service Observable qui contient l'état global de votre application.
- Vous
dispatchdes actions au store pour déclencher des changements d'état. - Vous
selectdes parties de l'état du store pour les afficher dans vos composants.
Actions (@ngrx/store)
Une action est un événement unique qui décrit ce qui s'est passé dans l'application. Elles sont le seul moyen de déclencher un changement d'état.
- Elles doivent avoir une propriété
typequi décrit l'événement (ex:'[Produit Page] Charger Produits','[Produit API] Produits Chargés Succès'). - Elles peuvent contenir un
payload(charge utile) avec des données pertinentes pour l'événement. - Utilisez
createActionpour créer des actions de manière type-safe.
Exemple :
// src/app/produits/produit.actions.ts
import { createAction, props } from '@ngrx/store';
import { Produit } from './models/produit.model';
export const chargerProduits = createAction(
'[Produit Page] Charger Produits'
);
export const chargerProduitsSucces = createAction(
'[Produit API] Produits Chargés Succès',
props<{ produits: Produit[] }>() // payload contenant la liste des produits
);
export const chargerProduitsEchec = createAction(
'[Produit API] Produits Chargés Échec',
props<{ error: any }>() // payload contenant l'erreur
);
Reducers (@ngrx/store)
Les reducers sont des fonctions pures qui prennent l'état actuel et une action, et retournent un nouvel état. Ils sont les seuls à pouvoir modifier l'état directement.
- Ils doivent être pures : pas d'effets secondaires (pas de mutations directes, pas d'appels API, pas de Date.now()).
- Ils sont responsables de la transition de l'état
SversS'en fonction de l'actionA. - Utilisez
createReduceretonpour définir la logique du reducer de manière concise et lisible.
Exemple :
// src/app/produits/produit.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { chargerProduits, chargerProduitsSucces, chargerProduitsEchec } from './produit.actions';
import { Produit } from './models/produit.model';
export interface ProduitState {
produits: Produit[];
loading: boolean;
error: any;
}
export const initialProduitState: ProduitState = {
produits: [],
loading: false,
error: null,
};
export const produitReducer = createReducer(
initialProduitState,
on(chargerProduits, state => ({ ...state, loading: true, error: null })), // Mise à jour de l'état 'loading'
on(chargerProduitsSucces, (state, { produits }) => ({ ...state, produits, loading: false })), // Mise à jour des produits et fin du chargement
on(chargerProduitsEchec, (state, { error }) => ({ ...state, error, loading: false })), // Enregistrement de l'erreur et fin du chargement
);
Selectors (@ngrx/store)
Les selectors sont des fonctions pures qui permettent d'extraire des parties spécifiques de l'état du store.
- Ils sont optimisés pour la performance grâce à la mémoïsation (ils recalculent la valeur seulement si les parties de l'état dont ils dépendent ont changé).
- Ils peuvent également transformer ou combiner différentes parties de l'état.
- Utilisez
createFeatureSelectorpour obtenir une tranche de l'état d'un "feature" (module) etcreateSelectorpour composer des sélecteurs.
Exemple :
// src/app/produits/produit.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProduitState } from './produit.reducer';
// Sélectionne la tranche de l'état pour les produits (assumant qu'elle est enregistrée sous la clé 'produits')
export const selectProduitState = createFeatureSelector<ProduitState>('produits');
// Sélectionne la liste des produits
export const selectTousLesProduits = createSelector(
selectProduitState,
(state: ProduitState) => state.produits
);
// Sélectionne l'état de chargement
export const selectProduitsLoading = createSelector(
selectProduitState,
(state: ProduitState) => state.loading
);
// Sélectionne l'erreur
export const selectProduitsError = createSelector(
selectProduitState,
(state: ProduitState) => state.error
);
Effects (@ngrx/effects)
Les Effects sont des classes qui gèrent les effets secondaires de l'application (side effects), c'est-à-dire les opérations qui ne sont pas pures et qui interagissent avec le monde extérieur. Cela inclut :
- Appels API HTTP
- Interactions avec le localStorage
- Opérations de routage
- Timers, WebSockets, etc.
Les Effects écoutent les actions dispatchées, exécutent une opération asynchrone, puis dispatchent de nouvelles actions (par exemple, pour indiquer le succès ou l'échec de l'opération) qui à leur tour seront traitées par les reducers.
Exemple :
// src/app/produits/produit.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { ProduitService } from './services/produit.service'; // Service d'API REST
import * as ProduitActions from './produit.actions';
@Injectable()
export class ProduitEffects {
constructor(
private actions$: Actions, // Flux de toutes les actions dispatchées
private produitService: ProduitService
) {}
chargerProduits$ = createEffect(() =>
this.actions$.pipe(
ofType(ProduitActions.chargerProduits), // Écoute l'action 'chargerProduits'
mergeMap(() =>
this.produitService.getProduits().pipe( // Appelle le service pour charger les produits
map(produits => ProduitActions.chargerProduitsSucces({ produits })), // Si succès, dispatch 'chargerProduitsSucces'
catchError(error => of(ProduitActions.chargerProduitsEchec({ error }))) // Si échec, dispatch 'chargerProduitsEchec'
)
)
)
);
}
Flux de données NgRx (Unidirectional Data Flow)
Le cycle de vie d'une opération avec NgRx suit le flux unidirectionnel du pattern Redux :
- Interaction Utilisateur / Événement : Un utilisateur clique sur un bouton ou une donnée arrive d'un service.
- Dispatch d'Action : Un composant ou un service dispatche une
Actionpour décrire ce qui s'est passé (ex:[User Component] Load User). - Effects (pour les effets secondaires) : Si l'action dispatchée nécessite une opération asynchrone (comme un appel API), un
Effectécoute cette action. L'Effect exécute l'opération (ex:HTTP GET /api/user).- Une fois l'opération terminée, l'Effect dispatche une nouvelle Action pour indiquer le résultat (ex:
[User API] Load User Successou[User API] Load User Failure).
- Une fois l'opération terminée, l'Effect dispatche une nouvelle Action pour indiquer le résultat (ex:
- Reducers : Que l'action provienne directement du composant ou d'un Effect, tous les
Reducerspertinents sont appelés. Chaque Reducer prend l'état actuel et l'Action, et retourne un nouvel état immuable. - Store : Le Store met à jour son état interne avec le nouvel état retourné par les Reducers.
- Selectors : Les composants s'abonnent aux
Selectorspour obtenir des parties spécifiques de l'état du Store. Lorsque l'état change, les Selectors renvoient les nouvelles valeurs. - Mise à jour de l'UI : Les composants reçoivent les nouvelles données via leurs Observables (souvent asynchrone via le pipe
async), et l'interface utilisateur est mise à jour pour refléter le nouvel état.
Ce cycle garantit que l'état est toujours prévisible et que chaque changement est traçable.
Exemple Pratique NgRx : Un Compteur Simple
Nous allons construire une application de compteur basique avec NgRx.
1. Définir l'état
Nous avons besoin d'une interface pour notre état et d'une valeur initiale.
// src/app/counter/counter.state.ts
export interface CounterState {
count: number;
}
export const initialCounterState: CounterState = {
count: 0
};
2. Créer des Actions
Trois actions : incrémenter, décrémenter, réinitialiser.
// src/app/counter/counter.actions.ts
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset');
3. Écrire le Reducer
Le reducer prend l'état actuel et l'action pour produire un nouvel état.
// src/app/counter/counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
import { initialCounterState } from './counter.state';
export const counterReducer = createReducer(
initialCounterState,
on(increment, state => ({ ...state, count: state.count + 1 })),
on(decrement, state => ({ ...state, count: state.count - 1 })),
on(reset, state => ({ ...state, count: 0 }))
);
Explication du code :
createReducer: Prend l'état initial et une série de fonctionson.on(action, (state) => ...): Chaque fonctionondéfinit comment l'état doit changer lorsqu'une action spécifique est dispatchée.{ ...state, count: state.count + 1 }: C'est une syntaxe JavaScript pour créer un nouvel objet d'état. Le...statecopie toutes les propriétés de l'état précédent, puiscount: state.count + 1surcharge la propriétécountavec la nouvelle valeur. Cela garantit l'immutabilité de l'état.
4. Créer des Selectors
Un sélecteur pour récupérer la valeur actuelle du compteur.
// src/app/counter/counter.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CounterState } from './counter.state';
// Sélectionne la tranche de l'état associée à la clé 'counter'
export const selectCounterState = createFeatureSelector<CounterState>('counter');
// Sélectionne la propriété 'count' à partir de l'état du compteur
export const selectCount = createSelector(
selectCounterState,
(state: CounterState) => state.count
);
Explication du code :
createFeatureSelector<CounterState>('counter'): Crée un sélecteur qui accède à la partie de l'état global qui est enregistrée sous la clé'counter'.createSelector(selectCounterState, (state: CounterState) => state.count): Crée un sélecteur qui prend leselectCounterStatecomme source et extrait la propriétécountde cet état. La mémoïsation decreateSelectorgarantit queselectCountne sera recalculé que siselectCounterStatea lui-même changé.
5. Intégrer dans un Composant
Le composant va dispatcher des actions et afficher la valeur du compteur.
// src/app/counter/counter.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { increment, decrement, reset } from './counter.actions';
import { selectCount } from './counter.selectors';
import { CounterState } from './counter.state'; // Import de l'interface d'état
@Component({
selector: 'app-counter',
template: `
<div style="text-align:center;">
<h1>Compteur: {{ count$ | async }}</h1>
<button (click)="increment()">Incrémenter</button>
<button (click)="decrement()">Décrémenter</button>
<button (click)="reset()">Réinitialiser</button>
</div>
`
})
export class CounterComponent {
count$: Observable<number>;
// Injecte le Store
constructor(private store: Store<{ counter: CounterState }>) {
// Sélectionne la valeur du compteur depuis le Store et l'assigne à un Observable
this.count$ = this.store.select(selectCount);
}
// Méthodes pour dispatcher les actions
increment() {
this.store.dispatch(increment());
}
decrement() {
this.store.dispatch(decrement());
}
reset() {
this.store.dispatch(reset());
}
}
Explication du code :
private store: Store<{ counter: CounterState }>: LeStoreest injecté et typé avec la structure de l'état global. Dans notre cas, il y a une tranchecounterde typeCounterState.this.count$ = this.store.select(selectCount);: Le composant s'abonne à la valeur du compteur via le sélecteurselectCount. Commeselectretourne unObservable, nous pouvons l'utiliser directement dans le template avec le pipeasync({{ count$ | async }}). Le pipeasyncgère automatiquement la souscription et la désouscription.this.store.dispatch(action());: Chaque fois qu'un bouton est cliqué, l'action correspondante est dispatchée au store, déclenchant le cycle NgRx.
6. Configurer le Module Root
Pour que NgRx fonctionne, nous devons enregistrer le reducer dans le module racine de l'application (AppModule).
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store'; // Import de StoreModule
import { counterReducer } from './counter/counter.reducer'; // Import du reducer
import { AppComponent } from './app.component';
import { CounterComponent } from './counter/counter.component'; // Import du composant
@NgModule({
declarations: [
AppComponent,
CounterComponent // Déclare le composant
],
imports: [
BrowserModule,
// Enregistre le reducer du compteur dans le Store global
StoreModule.forRoot({ counter: counterReducer })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Explication du code :
StoreModule.forRoot({ counter: counterReducer }): Cette ligne enregistre lecounterReducersous la clé'counter'dans l'état global de l'application. C'est ainsi quecreateFeatureSelector<CounterState>('counter')dans les sélecteurs sait quelle partie de l'état récupérer. Pour les modules "feature" (lazy-loaded), on utiliseraitStoreModule.forFeature().
Une fois ces étapes réalisées, vous pouvez démarrer votre application Angular (ng serve) et interagir avec le compteur. Chaque clic déclenchera une action, qui sera traitée par le reducer pour mettre à jour le store, et le composant réagira à ces changements en affichant la nouvelle valeur.
Alternatives à NgRx
Bien que NgRx soit puissant et largement adopté, il introduit une certaine complexité et une quantité de boilerplate qui peuvent être excessives pour des applications plus petites ou des équipes moins expérimentées avec le pattern Redux. Heureusement, d'autres solutions existent :
Quand NgRx est-il approprié ?
- Applications de grande taille et complexes : Où la gestion d'état est un défi majeur.
- Équipes importantes : Où la standardisation du flux de données est essentielle pour la collaboration.
- Besoin de débogage avancé : Les Redux DevTools sont incroyablement puissants.
- Besoin d'auditabilité et de prévisibilité strictes : NgRx impose une discipline qui réduit les bugs liés à l'état.
RxAngular
RxAngular n'est pas une bibliothèque de gestion d'état au sens "Redux" complet, mais un ensemble d'outils optimisés pour la performance des applications Angular utilisant RxJS. Il inclut des fonctionnalités comme :
RxState: Un service pour gérer l'état local d'un composant ou d'un service avec RxJS de manière réactive, réduisant le besoin de classes de service dédiées pour des états simples.pushetletdirectives : Des alternatives au pipeasyncpour un meilleur contrôle de la détection de changements et une meilleure performance.LocalState: Une approche pour gérer l'état local et les effets secondaires d'un composant.
Quand l'utiliser ? Quand la performance est critique, pour optimiser la détection de changements, ou pour gérer l'état au niveau du composant de manière plus robuste que de simples BehaviorSubject. Il peut être combiné avec NgRx.
Akita
Akita est une bibliothèque de gestion d'état inspirée de Redux et Vuex, mais avec une API beaucoup plus simple et moins de boilerplate que NgRx. Elle se veut plus pragmatique et plus orientée objet.
- Elle utilise des
Storespour encapsuler l'état, desQueriespour le sélectionner, et desActionspour le modifier. - Elle est particulièrement efficace pour la gestion des collections d'entités avec son
EntityStore.
Quand l'utiliser ? Pour des applications de taille moyenne à grande qui recherchent les avantages de Redux (unidirectional data flow, centralisation) mais avec une courbe d'apprentissage plus douce et moins de code à écrire.
NGXS
NGXS est une autre bibliothèque de gestion d'état inspirée de Redux, qui se veut plus "Angular-like" en utilisant des classes, des décorateurs et en réduisant la quantité de boilerplate par rapport à NgRx.
- Elle combine les concepts d'Actions, de Reducers et d'Effects en une seule classe
@State. - Elle est souvent perçue comme un bon compromis entre la puissance de NgRx et la simplicité d'Akita.
Quand l'utiliser ? Si vous aimez le modèle Redux mais préférez une approche plus idiomatique à Angular avec moins de fichiers et une structure plus compacte.
State "Vanilla" (RxJS simple)
Pour les applications plus petites ou pour la gestion d'état localisée à un service ou un composant, l'utilisation directe de RxJS (notamment BehaviorSubject ou ReplaySubject) peut être amplement suffisante.
- Un
BehaviorSubjectmaintient toujours une valeur actuelle et émet cette valeur aux nouveaux souscripteurs, ainsi que toutes les valeurs futures. - Un
ReplaySubjectpeut rejouer un certain nombre de valeurs passées aux nouveaux souscripteurs.
Exemple de gestion d'état simple avec RxJS
Considérons un service gérant l'état de connexion de l'utilisateur.
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
// BehaviorSubject pour stocker l'utilisateur courant, initialisé à null (non connecté)
private _currentUser = new BehaviorSubject<User | null>(null);
// Observable public pour permettre aux composants de s'abonner sans pouvoir modifier le Subject directement
readonly currentUser$: Observable<User | null> = this._currentUser.asObservable();
constructor() {
// Simuler le chargement initial de l'utilisateur (par exemple, depuis le localStorage)
setTimeout(() => {
console.log('Utilisateur initial chargé.');
this._currentUser.next({ id: 1, name: 'Jean Dupont', email: 'jean.dupont@example.com' });
}, 500);
}
login(user: User) {
console.log('Connexion de l\'utilisateur:', user.name);
this._currentUser.next(user); // Met à jour l'utilisateur courant
}
logout() {
console.log('Déconnexion de l\'utilisateur.');
this._currentUser.next(null); // Définir l'utilisateur à null
}
}
// src/app/components/user-profile/user-profile.component.ts
import { Component } from '@angular/core';
import { UserService } from '../../services/user.service';
import { Observable } from 'rxjs';
import { CommonModule } from '@angular/common'; // Nécessaire pour *ngIf
@Component({
selector: 'app-user-profile',
standalone: true, // Exemple avec un composant standalone
imports: [CommonModule],
template: `
<div *ngIf="userService.currentUser$ | async as user; else loggedOut">
<h2>Bienvenue, {{ user.name }}</h2>
<p>Email: {{ user.email }}</p>
<button (click)="userService.logout()">Déconnexion</button>
</div>
<ng-template #loggedOut>
<p>Vous n'êtes pas connecté.</p>
<button (click)="loginAsGuest()">Connexion en tant qu'invité</button>
</ng-template>
`
})
export class UserProfileComponent {
// Injecte le UserService, le rendant public pour un accès facile dans le template
constructor(public userService: UserService) {}
loginAsGuest() {
this.userService.login({ id: 2, name: 'Invité', email: 'guest@example.com' });
}
}
Explication du code :
UserService: Contient unBehaviorSubjectprivé (_currentUser) pour la gestion de l'état, et unObservablepublic (currentUser$) pour permettre aux composants de s'abonner. Les méthodesloginetlogoutappellentnext()sur leBehaviorSubjectpour émettre de nouvelles valeurs.UserProfileComponent: Injecte leUserServiceet utilise le pipeasync(userService.currentUser$ | async) pour afficher l'état de l'utilisateur et réagir aux changements.
Quand est-ce suffisant ?
- Petites applications ou modules spécifiques avec une logique d'état simple.
- État localisé qui ne nécessite pas d'être partagé de manière complexe ou débogué à travers des outils avancés.
- Quand la surcouche de complexité et de boilerplate d'une bibliothèque complète n'est pas justifiée par les besoins du projet.
Conclusion
La gestion d'état est un aspect fondamental du développement d'applications Angular robustes et maintenables. Comprendre le flux de données unidirectionnel et les principes d'immutabilité est crucial, quelle que soit la solution choisie.
NgRx offre une solution complète et puissante pour les applications Angular complexes, en imposant une structure rigoureuse inspirée de Redux. Ses briques fondamentales – Actions, Reducers, Selectors et Effects – travaillent de concert pour garantir un état prévisible et débogable.
Cependant, la "taille unique" n'existe pas. Des alternatives comme Akita, NGXS, RxAngular, ou même une simple gestion de l'état basée sur RxJS, peuvent être plus adaptées en fonction de la taille de votre projet, de la complexité de l'état à gérer et des préférences de votre équipe.
Le choix de la bonne stratégie de gestion d'état est une décision architecturale importante qui peut avoir un impact significatif sur la maintenabilité et la scalabilité de votre application Angular à long terme. Familiarisez-vous avec les concepts, testez les différentes options et choisissez celle qui répond le mieux aux besoins spécifiques de votre projet.