Générez le boilerplate NgRx complet (Actions, Reducer, Effects, Selectors) pour votre feature Angular. Code TypeScript prêt à l'emploi avec EntityAdapter.
Générateur NgRx Boilerplate Angular
🔧 Configuration
📋 Opérations CRUD
⚙️ Options
NgRx et le pattern Redux dans Angular
NgRx est la bibliothèque de gestion d'état la plus populaire pour Angular. Elle implémente le pattern Redux — inspiré de Flux — qui repose sur un principe simple : l'état de l'application est stocké dans un objet unique et immuable, le Store. Toute modification passe par des Actions explicites qui déclenchent des Reducers purs, garantissant ainsi une traçabilité totale et une testabilité maximale.
Adopter NgRx dans vos projets Angular apporte plusieurs bénéfices concrets :
- ✅ Prévisibilité — chaque action produit un état déterministe
- ✅ Débogage avancé — Redux DevTools avec time-travel debugging
- ✅ Séparation des préoccupations — la logique async est isolée dans les Effects
- ✅ Performance — Selectors mémorisés avec
createSelector - ✅ Scalabilité — architecture features indépendantes
Architecture NgRx : les 5 piliers
NgRx s'articule autour de cinq concepts fondamentaux qui forment un cycle de données unidirectionnel (unidirectional data flow) :
| Concept | Rôle | Fichier |
|---|---|---|
| Store | Source unique de vérité — contient l'état global | app.config.ts |
| Actions | Événements qui décrivent ce qui s'est passé | *.actions.ts |
| Reducers | Fonctions pures qui calculent le nouvel état | *.reducer.ts |
| Effects | Gèrent les effets de bord (API, localStorage…) | *.effects.ts |
| Selectors | Fonctions mémorisées pour lire/dériver l'état | *.selectors.ts |
Le cycle complet : Component → dispatch une Action → le Reducer calcule le nouvel état → le Store met à jour → les Selectors notifient les composants abonnés. Les Effects interceptent certaines actions pour appeler l'API, puis dispatchent des actions de succès ou d'erreur.
Actions NgRx : conventions et bonnes pratiques
Les Actions NgRx sont définies avec createAction() depuis @ngrx/store. Chaque action possède un type unique — conventionnellement au format [Source] Événement — et des props optionnelles via props<T>().
La convention de nommage des types d'actions suit la structure [Feature] Verbe Entité [Résultat] :
// ✅ Convention recommandée
export const loadProducts = createAction('[Products] Load Products');
export const loadProductsSuccess = createAction(
'[Products] Load Products Success',
props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
'[Products] Load Products Failure',
props<{ error: string }>()
);
// ✅ Action avec payload complexe
export const updateProduct = createAction(
'[Products] Update Product',
props<{ id: number; changes: Partial<Product> }>()
);
- Toujours grouper par 3 : action principale + Success + Failure
- Utiliser
Omit<T, 'id'>pour les actions de création (l'ID vient du serveur) - Préférer
Partial<T>pour les updates partiels (PATCH) - Le type de l'action doit être lisible dans Redux DevTools
- Exporter la clé de feature avec
featureKeypour la cohérence
Reducer et @ngrx/entity : gestion optimale des collections
Le Reducer est une fonction pure (state, action) => newState. NgRx propose createReducer() combiné à on() pour un code déclaratif et lisible. Pour les collections d'entités, @ngrx/entity fournit un EntityAdapter qui optimise les opérations CRUD sur les tableaux d'objets.
// Reducer avec EntityAdapter
import { createReducer, on } from '@ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { Product } from '../models/product.model';
import * as ProductActions from './product.actions';
export interface ProductsState extends EntityState<Product> {
loading: boolean;
error: string | null;
selectedId: number | null;
}
// L'adapter gère l'ID automatiquement (id par défaut)
export const adapter: EntityAdapter<Product> = createEntityAdapter<Product>();
export const initialState: ProductsState = adapter.getInitialState({
loading: false,
error: null,
selectedId: null,
});
export const productsReducer = createReducer(
initialState,
on(ProductActions.loadProducts, state =>
({ ...state, loading: true, error: null })),
// adapter.setAll() remplace toute la collection en O(n)
on(ProductActions.loadProductsSuccess, (state, { products }) =>
adapter.setAll(products, { ...state, loading: false })),
on(ProductActions.loadProductsFailure, (state, { error }) =>
({ ...state, loading: false, error })),
// adapter.addOne() ajoute un élément sans mutation
on(ProductActions.createProductSuccess, (state, { product }) =>
adapter.addOne(product, { ...state, loading: false })),
// adapter.upsertOne() crée ou met à jour
on(ProductActions.updateProductSuccess, (state, { product }) =>
adapter.upsertOne(product, { ...state, loading: false })),
// adapter.removeOne() supprime par ID
on(ProductActions.deleteProductSuccess, (state, { id }) =>
adapter.removeOne(id, { ...state, loading: false })),
);
L'EntityAdapter stocke les entités dans un format normalisé { ids: number[], entities: Record<number, Entity> } — bien plus performant qu'un tableau brut pour les recherches par ID.
id comme identifiant, configurez l'adapter : createEntityAdapter<Product>({ selectId: p => p.uuid })
Effects NgRx : logique asynchrone avec RxJS
Les Effects interceptent les actions du Store pour exécuter des opérations asynchrones (appels HTTP, accès localStorage, WebSocket…) et dispatcher de nouvelles actions en résultat. Ils s'appuient sur RxJS pour composer les flux de données de manière déclarative.
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { catchError, map, switchMap, mergeMap } from 'rxjs/operators';
import { ProductService } from '../services/product.service';
import * as ProductActions from './product.actions';
@Injectable()
export class ProductEffects {
// switchMap annule l'appel précédent si une nouvelle action arrive
// Idéal pour les lectures (GET list, GET by id)
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.loadProducts),
switchMap(() =>
this.productService.getAll().pipe(
map(products => ProductActions.loadProductsSuccess({ products })),
catchError(err => of(ProductActions.loadProductsFailure({ error: err.message })))
)
)
)
);
// mergeMap exécute chaque appel en parallèle
// Idéal pour les mutations (POST, PUT, DELETE)
createProduct$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.createProduct),
mergeMap(({ product }) =>
this.productService.create(product).pipe(
map(result => ProductActions.createProductSuccess({ product: result })),
catchError(err => of(ProductActions.createProductFailure({ error: err.message })))
)
)
)
);
constructor(
private readonly actions$: Actions,
private readonly productService: ProductService
) {}
}
| Opérateur RxJS | Comportement | Usage NgRx |
|---|---|---|
switchMap |
Annule le flux précédent | GET (recherche, chargement liste) |
mergeMap |
Exécute en parallèle | POST, PUT, DELETE |
concatMap |
Exécute séquentiellement | Opérations ordonnées (upload multi-fichiers) |
exhaustMap |
Ignore si déjà en cours | Formulaires de connexion (évite double-submit) |
Selectors mémorisés : lire l'état efficacement
Les Selectors sont des fonctions pures qui extraient et transforment des portions du Store. createSelector() les mémorise (memoize) : si les entrées n'ont pas changé, le résultat précédent est réutilisé sans recalcul — optimisation cruciale pour les grandes collections.
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { adapter, ProductsState } from './products.reducer';
// Sélecteur racine de la feature
export const selectProductsState =
createFeatureSelector<ProductsState>('products');
// Sélecteurs EntityAdapter (optimisés)
export const {
selectAll: selectAllProducts,
selectEntities: selectProductEntities,
selectIds: selectProductIds,
selectTotal: selectProductsTotal,
} = adapter.getSelectors(selectProductsState);
// Sélecteurs dérivés (mémorisés)
export const selectProductsLoading = createSelector(
selectProductsState,
state => state.loading
);
export const selectSelectedProductId = createSelector(
selectProductsState,
state => state.selectedId
);
// Composition de sélecteurs
export const selectSelectedProduct = createSelector(
selectProductEntities,
selectSelectedProductId,
(entities, id) => (id != null ? entities[id] : null)
);
// Sélecteur avec transformation
export const selectActiveProducts = createSelector(
selectAllProducts,
products => products.filter(p => p.active)
);
// Usage dans un composant
export class ProductListComponent {
products$ = this.store.select(selectAllProducts);
loading$ = this.store.select(selectProductsLoading);
constructor(private store: Store) {
this.store.dispatch(loadProducts());
}
}
createSelector(). Pour des cas plus complexes, combinez plusieurs createSelector() entre eux.
Intégrer NgRx dans un projet Angular
Installez NgRx via la CLI Angular — les schematics configurent automatiquement StoreModule et EffectsModule :
# Installation complète NgRx (core + entity + effects + devtools)
ng add @ngrx/store@latest
ng add @ngrx/effects@latest
ng add @ngrx/entity@latest
ng add @ngrx/store-devtools@latest
# Générer une feature NgRx avec les schematics officiels
ng generate @ngrx/schematics:feature products \
--module=app.module.ts \
--api # génère Effects avec appel HTTP
--entity # utilise EntityAdapter
Avec Angular 17+ en mode Standalone, la configuration se fait dans app.config.ts :
// app.config.ts — configuration standalone NgRx 17+
import { ApplicationConfig } from '@angular/core';
import { provideStore, provideState } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productsReducer } from './products/product.reducer';
import { ProductEffects } from './products/product.effects';
import { isDevMode } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideStore(), // Store racine vide
provideState('products', productsReducer), // Feature slice
provideEffects(ProductEffects), // Effects de la feature
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
],
};
products/models/product.model.ts— Interface TypeScriptproducts/services/product.service.ts— Service HTTP Angularproducts/store/product.actions.ts— Déclarations d'actionsproducts/store/product.reducer.ts— Reducer + initialStateproducts/store/product.effects.ts— Effets asynchronesproducts/store/product.selectors.ts— Sélecteurs mémorisésproducts/store/index.ts— Barrel re-export
FAQ NgRx
ng add @ngrx/store@latest installe automatiquement la version compatible. Les versions 16+ supportent les Signals via selectSignal().
- NgRx Store : état global partagé à l'échelle de l'application
- ComponentStore : état local à un composant/feature, plus simple que NgRx global
- Signal Store (NgRx 17+) : alternative moderne basée sur les Signals Angular, moins de boilerplate, idéal pour les nouvelles applications
provideStoreDevtools() dans votre config. Vous pouvez alors visualiser toutes les actions dispatchées, inspecter l'état avant/après chaque action, et voyager dans le temps (time-travel debugging) pour reproduire des bugs.
id. Pour un UUID ou un autre champ, configurez le sélecteur d'ID :
createEntityAdapter<Product>({ selectId: p => p.uuid })
Vous pouvez aussi trier les entités à l'initialisation avec sortComparer :
createEntityAdapter<Product>({
sortComparer: (a, b) => a.name.localeCompare(b.name)
})
interface ProductsState extends EntityState<Product> {
loading: boolean;
error: string | null;
pagination: {
currentPage: number;
pageSize: number;
totalItems: number;
};
}
Puis créez des actions dédiées : loadProductsPage avec props<{ page: number; size: number }>().
Pour les Effects, utilisez
provideMockActions() de @ngrx/effects/testing et hot()/cold() de jasmine-marbles pour simuler des observables RxJS dans les tests unitaires.
Conclusion
Ce générateur NgRx vous fournit un boilerplate TypeScript propre et prêt à l'emploi pour vos features Angular. Il couvre les 5 piliers essentiels — Actions, Reducer avec EntityAdapter, Effects RxJS, Selectors mémorisés et la configuration du module — en vous laissant choisir précisément les opérations CRUD dont vous avez besoin.
Pour aller plus loin, consultez la documentation officielle NgRx et explorez les NgRx Schematics pour automatiser la génération de code directement depuis la CLI Angular.