Angular State Management
State management is crucial for maintaining predictable application behavior as your Angular app grows. You can choose between simple services or more advanced solutions like NgRx.
State Management Options
- Services with BehaviorSubject: Simple and effective for small to medium apps
- NgRx: Redux-inspired state management for complex apps
- Akita: Simpler alternative to NgRx
- NGXS: Another state management pattern
Service with BehaviorSubject
A simple state management solution using RxJS:
// products.state.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class ProductsState {
private products$ = new BehaviorSubject<Product[]>([]);
private loading$ = new BehaviorSubject<boolean>(false);
private error$ = new BehaviorSubject<string | null>(null);
// Selectors
getProducts$() {
return this.products$.asObservable();
}
getLoading$() {
return this.loading$.asObservable();
}
getError$() {
return this.error$.asObservable();
}
// Actions
setLoading(loading: boolean) {
this.loading$.next(loading);
}
setProducts(products: Product[]) {
this.products$.next(products);
}
setError(error: string | null) {
this.error$.next(error);
}
addProduct(product: Product) {
const currentProducts = this.products$.getValue();
this.products$.next([...currentProducts, product]);
}
updateProduct(updatedProduct: Product) {
const products = this.products$.getValue();
this.products$.next(
products.map(product =>
product.id === updatedProduct.id ? updatedProduct : product
)
);
}
deleteProduct(id: number) {
const products = this.products$.getValue();
this.products$.next(products.filter(product => product.id !== id));
}
}
Using the State Service
In your component or service:
// products.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ProductsState } from './products.state';
@Injectable({ providedIn: 'root' })
export class ProductsService {
constructor(
private http: HttpClient,
private productsState: ProductsState
) { }
loadProducts() {
this.productsState.setLoading(true);
this.productsState.setError(null);
this.http.get<Product[]>('https://api.example.com/products')
.subscribe({
next: products => {
this.productsState.setProducts(products);
this.productsState.setLoading(false);
},
error: error => {
this.productsState.setError('Failed to load products');
this.productsState.setLoading(false);
}
});
}
}
// products.component.ts
import { Component } from '@angular/core';
import { ProductsState } from './products.state';
@Component({
selector: 'app-products',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error">{{error}}</div>
<ul>
<li *ngFor="let product of products$ | async">
{{product.name}} - {{product.price}}
</li>
</ul>
`
})
export class ProductsComponent {
products$ = this.productsState.getProducts$();
loading$ = this.productsState.getLoading$();
error$ = this.productsState.getError$();
constructor(private productsState: ProductsState) { }
ngOnInit() {
this.productsState.loadProducts();
}
}
NgRx Setup
Install NgRx packages:
npm install @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/entity
NgRx Store Structure
NgRx follows the Redux pattern with actions, reducers, effects, and selectors:
src/
└── app/
└── store/
├── actions/
│ └── products.actions.ts
├── reducers/
│ ├── products.reducer.ts
│ └── index.ts
├── effects/
│ └── products.effects.ts
├── selectors/
│ └── products.selectors.ts
└── models/
└── product.model.ts
NgRx Actions
// products.actions.ts
import { createAction, props } from '@ngrx/store';
import { Product } from '../models/product.model';
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 }>()
);
NgRx Reducer
// products.reducer.ts
import { createReducer, on } from '@ngrx/store';
import * as ProductsActions from '../actions/products.actions';
import { Product } from '../models/product.model';
export interface ProductsState {
products: Product[];
loading: boolean;
error: string | null;
}
export const initialState: ProductsState = {
products: [],
loading: false,
error: null
};
export const productsReducer = createReducer(
initialState,
on(ProductsActions.loadProducts, state => ({
...state,
loading: true,
error: null
})),
on(ProductsActions.loadProductsSuccess, (state, { products }) => ({
...state,
products,
loading: false
})),
on(ProductsActions.loadProductsFailure, (state, { error }) => ({
...state,
error,
loading: false
}))
);
NgRx Effects
// products.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 { HttpClient } from '@angular/common/http';
import * as ProductsActions from '../actions/products.actions';
@Injectable()
export class ProductsEffects {
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductsActions.loadProducts),
mergeMap(() =>
this.http.get<Product[]>('https://api.example.com/products').pipe(
map(products => ProductsActions.loadProductsSuccess({ products })),
catchError(error => of(ProductsActions.loadProductsFailure({ error: error.message })))
)
)
)
);
constructor(
private actions$: Actions,
private http: HttpClient
) {}
}
NgRx Selectors
// products.selectors.ts
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { ProductsState } from '../reducers/products.reducer';
export const selectProductsState = createFeatureSelector<ProductsState>('products');
export const selectProducts = createSelector(
selectProductsState,
(state: ProductsState) => state.products
);
export const selectLoading = createSelector(
selectProductsState,
(state: ProductsState) => state.loading
);
export const selectError = createSelector(
selectProductsState,
(state: ProductsState) => state.error
);
Registering NgRx Store
// app.module.ts
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { productsReducer } from './store/reducers/products.reducer';
import { ProductsEffects } from './store/effects/products.effects';
@NgModule({
imports: [
StoreModule.forRoot({ products: productsReducer }),
EffectsModule.forRoot([ProductsEffects]),
StoreDevtoolsModule.instrument({
maxAge: 25 // Retains last 25 states
})
]
})
export class AppModule { }
Using NgRx in Components
// products.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Product } from './store/models/product.model';
import * as ProductsActions from './store/actions/products.actions';
import { selectProducts, selectLoading, selectError } from './store/selectors/products.selectors';
@Component({
selector: 'app-products',
template: `
<div *ngIf="loading$ | async">Loading...</div>
<div *ngIf="error$ | async as error">{{error}}</div>
<ul>
<li *ngFor="let product of products$ | async">
{{product.name}} - {{product.price}}
</li>
</ul>
<button (click)="loadProducts()">Reload</button>
`
})
export class ProductsComponent {
products$: Observable<Product[]>;
loading$: Observable<boolean>;
error$: Observable<string | null>;
constructor(private store: Store) {
this.products$ = this.store.select(selectProducts);
this.loading$ = this.store.select(selectLoading);
this.error$ = this.store.select(selectError);
}
ngOnInit() {
this.loadProducts();
}
loadProducts() {
this.store.dispatch(ProductsActions.loadProducts());
}
}
Entity State with NgRx
For collections, use NgRx Entity for optimized state management:
// products.reducer.ts
import { createEntityAdapter, EntityState, EntityAdapter } from '@ngrx/entity';
export interface ProductsState extends EntityState<Product> {
loading: boolean;
error: string | null;
}
export const adapter: EntityAdapter<Product> = createEntityAdapter<Product>();
export const initialState: ProductsState = adapter.getInitialState({
loading: false,
error: null
});
export const productsReducer = createReducer(
initialState,
on(ProductsActions.loadProducts, state => ({
...state,
loading: true,
error: null
})),
on(ProductsActions.loadProductsSuccess, (state, { products }) =>
adapter.setAll(products, {
...state,
loading: false
})
),
// ... other cases
);
// Selectors
export const {
selectAll: selectAllProducts,
selectEntities: selectProductEntities,
selectIds: selectProductIds,
selectTotal: selectTotalProducts
} = adapter.getSelectors();
When to Use Which Approach
- Service with BehaviorSubject: Small to medium apps, simple state needs
- NgRx: Large apps with complex state, need for time-travel debugging
- Akita/NGXS: Alternative to NgRx with less boilerplate