NgRx — Redux-Style State Management
NgRx is the most popular state management library for Angular. It implements the Redux pattern using RxJS Observables, providing a predictable, immutable state container with powerful DevTools support.
NgRx is best suited for large applications with complex state that many components share, where features like time-travel debugging, action replay, and strict unidirectional data flow are worth the additional boilerplate.
Core NgRx Concepts
Concept | Role | Analogy |
|---|---|---|
Store | Single source of truth — holds all state | Database |
Action | Plain object describing what happened | Event / command |
Reducer | Pure function: (state, action) => newState | Event handler |
Selector | Pure function that reads a slice of state | SQL query |
Effect | Side effects (HTTP, routing) triggered by actions | Middleware |
Installation
ng add @ngrx/store@latest ng add @ngrx/effects@latest ng add @ngrx/store-devtools@latest # optional but highly recommended
Step 1 — Define the State Model
// src/app/store/products/product.model.ts
export interface Product {
id: number;
name: string;
price: number;
category: string;
}
export interface ProductState {
products: Product[];
selectedProduct: Product | null;
loading: boolean;
error: string | null;
}Step 2 — Define Actions
Actions are plain objects with a type string. Use createAction and props for type-safe action creators.
// src/app/store/products/product.actions.ts
import { createAction, props } from '@ngrx/store';
import { Product } from './product.model';
// Load all products
export const loadProducts = createAction('[Product List] Load Products');
export const loadProductsSuccess = createAction(
'[Product API] Load Products Success',
props<{ products: Product[] }>()
);
export const loadProductsFailure = createAction(
'[Product API] Load Products Failure',
props<{ error: string }>()
);
// Select a product
export const selectProduct = createAction(
'[Product List] Select Product',
props<{ productId: number }>()
);
// Create a product
export const createProduct = createAction(
'[Product Form] Create Product',
props<{ product: Omit<Product, 'id'> }>()
);
export const createProductSuccess = createAction(
'[Product API] Create Product Success',
props<{ product: Product }>()
);
// Delete a product
export const deleteProduct = createAction(
'[Product List] Delete Product',
props<{ productId: number }>()
);[Source] Event — the source (in brackets) makes actions easy to trace in DevTools.Step 3 — Create the Reducer
// src/app/store/products/product.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { ProductState } from './product.model';
import * as ProductActions from './product.actions';
export const initialState: ProductState = {
products: [],
selectedProduct: null,
loading: false,
error: null,
};
export const productReducer = createReducer(
initialState,
on(ProductActions.loadProducts, (state) => ({
...state,
loading: true,
error: null,
})),
on(ProductActions.loadProductsSuccess, (state, { products }) => ({
...state,
loading: false,
products,
})),
on(ProductActions.loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error,
})),
on(ProductActions.selectProduct, (state, { productId }) => ({
...state,
selectedProduct: state.products.find(p => p.id === productId) ?? null,
})),
on(ProductActions.createProductSuccess, (state, { product }) => ({
...state,
products: [...state.products, product],
})),
on(ProductActions.deleteProduct, (state, { productId }) => ({
...state,
products: state.products.filter(p => p.id !== productId),
}))
);Step 4 — Create Selectors
// src/app/store/products/product.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductState } from './product.model';
// Select the feature state slice
export const selectProductState = createFeatureSelector<ProductState>('products');
// Select individual pieces of state
export const selectAllProducts = createSelector(
selectProductState,
(state) => state.products
);
export const selectProductsLoading = createSelector(
selectProductState,
(state) => state.loading
);
export const selectProductsError = createSelector(
selectProductState,
(state) => state.error
);
export const selectSelectedProduct = createSelector(
selectProductState,
(state) => state.selectedProduct
);
// Derived / computed selectors
export const selectProductCount = createSelector(
selectAllProducts,
(products) => products.length
);
export const selectProductsByCategory = (category: string) => createSelector(
selectAllProducts,
(products) => products.filter(p => p.category === category)
);Step 5 — Create Effects
Effects handle side effects like HTTP calls. They listen for actions, perform async work, and dispatch new actions.
// src/app/store/products/product.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { HttpClient } from '@angular/common/http';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import * as ProductActions from './product.actions';
import { Product } from './product.model';
@Injectable()
export class ProductEffects {
private actions$ = inject(Actions);
private http = inject(HttpClient);
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.loadProducts),
switchMap(() =>
this.http.get<Product[]>('/api/products').pipe(
map(products => ProductActions.loadProductsSuccess({ products })),
catchError(error =>
of(ProductActions.loadProductsFailure({ error: error.message }))
)
)
)
)
);
createProduct$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.createProduct),
switchMap(({ product }) =>
this.http.post<Product>('/api/products', product).pipe(
map(created => ProductActions.createProductSuccess({ product: created })),
catchError(error =>
of(ProductActions.loadProductsFailure({ error: error.message }))
)
)
)
)
);
}Step 6 — Register in app.config.ts
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productReducer } from './store/products/product.reducer';
import { ProductEffects } from './store/products/product.effects';
import { isDevMode } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({ products: productReducer }),
provideEffects([ProductEffects]),
provideStoreDevtools({
maxAge: 25,
logOnly: !isDevMode(),
}),
],
};Step 7 — Use in a Component
// src/app/products/product-list.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { AsyncPipe, CurrencyPipe } from '@angular/common';
import { loadProducts, deleteProduct, selectProduct } from '../store/products/product.actions';
import {
selectAllProducts,
selectProductsLoading,
selectProductsError,
} from '../store/products/product.selectors';
@Component({
selector: 'app-product-list',
standalone: true,
imports: [AsyncPipe, CurrencyPipe],
template: `
@if (loading$ | async) { <p>Loading...</p> }
@if (error$ | async; as error) { <p class="error">{{ error }}</p> }
@for (product of products$ | async; track product.id) {
<div class="product-card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency }}</p>
<button (click)="onSelect(product.id)">View</button>
<button (click)="onDelete(product.id)">Delete</button>
</div>
}
`,
})
export class ProductListComponent implements OnInit {
private store = inject(Store);
products$ = this.store.select(selectAllProducts);
loading$ = this.store.select(selectProductsLoading);
error$ = this.store.select(selectProductsError);
ngOnInit(): void {
this.store.dispatch(loadProducts());
}
onSelect(productId: number): void {
this.store.dispatch(selectProduct({ productId }));
}
onDelete(productId: number): void {
this.store.dispatch(deleteProduct({ productId }));
}
}NgRx DevTools
Install the Redux DevTools browser extension to get time-travel debugging. You can replay actions, inspect state at any point, and import/export state snapshots.
Install Redux DevTools extension for Chrome or Firefox
Add provideStoreDevtools() in app.config.ts (already shown above)
Open DevTools → Redux tab
See every dispatched action and resulting state diff
Jump back in time by clicking any past action
NgRx Best Practices
Keep actions granular and descriptive — one action per user intent
Reducers must be pure functions — no side effects, no HTTP calls
Put all async logic (HTTP, routing) in Effects, not reducers
Use selectors for all state reads — never select state in effects directly
Memoized selectors (createSelector) prevent unnecessary recalculations
Co-locate actions, reducers, selectors, and effects per feature folder
Use the [Source] Event naming convention for action types
Only adopt NgRx if the complexity justifies it — start with services + signals
Summary
NgRx brings the full Redux pattern to Angular: a single immutable state tree, actions describing what happened, pure reducers computing the next state, and effects for side effects. The DevTools integration makes debugging production issues much easier. The tradeoff is boilerplate — plan for actions, reducers, selectors, and effects files per feature. NgRx is the right choice for large teams and complex applications where predictability and auditability outweigh the setup cost.