Pourquoi NgRx avec Angular : guide complet ?
NgRx est une bibliothèque state management très puissante et flexible pour les applications Angular. En tant qu'application front-end moderne, le state management devient rapidement un encombrement lorsque vous gérez des données complexes et réparties sur plusieurs composants. NgRx fournit une structure claire et prévisible pour gérer l'état de votre application, ce qui améliore la maintenabilité et la scalabilité.
Un cas d'usage concret est un e-commerce en ligne où le panier doit être partagé entre différents composants de l'application. Avec NgRx, vous pouvez centraliser le state du panier dans un seul store, facilitant ainsi la gestion des interactions entre les différents composants et garantissant que les données sont cohérentes.
Prerequis
- Connaissances en Angular (version 10+)
- Compréhension de RxJS
- Environnement de développement installé avec Node.js (v14+), npm (v6+), et Angular CLI (v10+)
Pour installer les outils nécessaires :
## Installer Node.js et npm
curl -fsSL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs
## Installer Angular CLI
npm install -g @angular/cli@10+
Concepts fondamentaux
1. Store
Le store est le seul endroit où l'état de votre application se trouve.
// src/app/store/index.ts
import { createStore, ActionReducerMap } from '@ngrx/store';
import * as fromCounter from './counter.reducer';
export interface AppState {
counter: fromCounter.State;
}
export const reducers: ActionReducerMap<AppState> = {
counter: fromCounter.reducer,
};
export function createAppStore() {
return createStore(reducers);
}
2. Reducer
Un reducer prend le state actuel et une action, puis retourne un nouveau state.
// src/app/store/counter.reducer.ts
import { Action } from '@ngrx/store';
export interface State {
count: number;
}
const initialState: State = {
count: 0,
};
export function reducer(state = initialState, action: Action): State {
switch (action.type) {
case '[Counter] Increment':
return {
...state,
count: state.count + 1,
};
case '[Counter] Decrement':
return {
...state,
count: state.count - 1,
};
default:
return state;
}
}
3. Action
Une action est un objet qui décrit ce qui a changé dans l'état.
// src/app/store/counter.actions.ts
export const increment = () => ({
type: '[Counter] Increment',
});
export const decrement = () => ({
type: '[Counter] Decrement',
});
4. Selector
Un selector permet d'extraire une partie spécifique du state.
// src/app/store/counter.selectors.ts
import { createSelector } from '@ngrx/store';
import * as fromCounter from './counter.reducer';
export const selectCounterState = (state: fromCounter.State) => state.counter;
export const selectCount = createSelector(
selectCounterState,
(counterState: fromCounter.State) => counterState.count
);
Mise en pratique : projet fil rouge
Étape 1 : Création du projet Angular
ng new ngRx-todo-app --routing
cd ngRx-todo-app
Étape 2 : Installation de NgRx
npm install @ngrx/store @ngrx/effects @ngrx/router-store @ngrx/entity --save
Étape 3 : Configuration du store
Créez un fichier store/index.ts pour configurer le store.
// src/app/store/index.ts
import { createStore, ActionReducerMap } from '@ngrx/store';
import * as fromTodos from './todos.reducer';
export interface AppState {
todos: fromTodos.State;
}
export const reducers: ActionReducerMap<AppState> = {
todos: fromTodos.reducer,
};
export function createAppStore() {
return createStore(reducers);
}
Étape 4 : Création du reducer
Créez un fichier todos.reducer.ts pour le reducer.
// src/app/store/todos.reducer.ts
import { Action } from '@ngrx/store';
export interface State {
ids: string[];
entities: { [id: string]: Todo };
}
export const initialState: State = {
ids: [],
entities: {},
};
export function reducer(state = initialState, action: Action): State {
switch (action.type) {
case '[Todos] Add Todo':
return {
...state,
ids: [...state.ids, action.payload.id],
entities: { ...state.entities, [action.payload.id]: action.payload },
};
default:
return state;
}
}
Étape 5 : Création des actions
Créez un fichier todos.actions.ts pour les actions.
// src/app/store/todos.actions.ts
import { Action } from '@ngrx/store';
export const addTodo = (id: string, title: string) => ({
type: '[Todos] Add Todo',
payload: { id, title },
});
Étape 6 : Création des selectors
Créez un fichier todos.selectors.ts pour les selectors.
// src/app/store/todos.selectors.ts
import { createSelector } from '@ngrx/store';
import * as fromTodos from './todos.reducer';
export const selectTodosState = (state: fromTodos.State) => state.todos;
export const selectAllTodos = createSelector(
selectTodosState,
(todosState: fromTodos.State) => Object.values(todosState.entities)
);
Étape 7 : Injection du store dans le composant
Créez un fichier app.component.ts pour le composant.
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import * as fromTodos from './store/todos.reducer';
import { addTodo } from './store/todos.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
todos$ = this.store.pipe(select(fromTodos.selectAllTodos));
constructor(private store: Store) {}
ngOnInit() {}
addTodo(title: string) {
const id = Date.now().toString();
this.store.dispatch(addTodo({ id, title }));
}
}
Étape 8 : Ajout du template
Créez un fichier app.component.html pour le template.
<!-- src/app/app.component.html -->
<div>
<h1>Todo List</h1>
<ul>
<li *ngFor="let todo of todos$ | async">
todo.title
<button (click)="removeTodo(todo.id)">Remove</button>
</li>
</ul>
<input type="text" [(ngModel)]="newTodoTitle" placeholder="Add new todo" />
<button (click)="addTodo(newTodoTitle)">Add Todo</button>
</div>
Erreurs frequentes et debugging
Erreur 1 : Action non dispatchée
// ❌ Mauvais
this.store.dispatch(addTodo({ id: '1', title: 'Test' }));
// ✅ Correct
this.store.dispatch(new AddTodo('1', 'Test'));
Erreur 2 : Selector non trouvé
// ❌ Mauvais
const todos$ = this.store.pipe(select(fromTodos.selectAllTodos));
// ✅ Correct
const todos$ = this.store.pipe(select(fromTodos.selectAllTodos));
Erreur 3 : Reducer non défini
// ❌ Mauvais
export const reducers: ActionReducerMap<AppState> = {
counter: fromCounter.reducer,
};
// ✅ Correct
export const reducers: ActionReducerMap<AppState> = {
todos: fromTodos.reducer,
};
Pour aller plus loin
1. Intégration avec Effects
NgRx Effects permet de gérer les effets secondaires (appels API, etc.) en séparant le flux d'actions et les actions déclenchées.
// src/app/store/todos.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as fromTodos from './todos.reducer';
import { addTodo } from './todos.actions';
@Injectable()
export class TodosEffects {
constructor(private actions$: Actions) {}
addTodo$ = createEffect(() =>
this.actions$.pipe(
ofType(fromTodos.addTodo),
tap((action) => {
// Effect code here
})
)
);
}
2. Utilisation de Feature Modules
NgRx permet de structurer le state en utilisant des modules feature.
// src/app/store/todos/todo.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import * as fromTodos from './todos.reducer';
import { TodosEffects } from './todos.effects';
@NgModule({
imports: [
StoreModule.forFeature(fromTodos.todosFeatureKey, fromTodos.reducer),
EffectsModule.forFeature([TodosEffects]),
],
})
export class TodosModule {}
3. Persisting State
NgRx persisting state permet de sauvegarder le state dans le local storage.
// src/app/store/persist-config.ts
import { StoreConfig } from '@ngrx/store';
import * as fromTodos from './todos.reducer';
export const todosFeatureKey = 'todos';
@StoreConfig({ name: todosFeatureKey, resetOnChanges: true })
export class TodosState {}
Défi pratique : Créer une application de gestion de contacts
Créez une application simple pour gérer des contacts. Utilisez NgRx pour gérer le state des contacts et ajoutez les fonctionnalités suivantes :
- Ajouter un contact
- Supprimer un contact
- Mettre à jour un contact
- Afficher la liste des contacts
Conseils :
- Structurez votre store en utilisant des feature modules.
- Utilisez NgRx Effects pour gérer les effets secondaires (appels API).
- Implémentez des selectors pour extraire des parties spécifiques du state.