AngularJSNgRx

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

Bash
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

TS
// 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.

TS
// 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 }>()
);
Note
The action type string convention is [Source] Event — the source (in brackets) makes actions easy to trace in DevTools.
Step 3 — Create the Reducer

TS
// 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

TS
// 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.

TS
// 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

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

TS
// 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

Warning
NgRx adds significant boilerplate. For apps with less than ~10 shared state slices, services with BehaviorSubject or Angular Signals are usually a better fit.
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.