Sitemap

State management with NgRx

4 min readFeb 4, 2025

What is NgRx?

NgRx is a state management library for Angular applications. It provides a way to manage the state of your application in a predictable and centralized manner. NgRx is based on the Redux pattern, which revolves around a unidirectional data flow and immutable state.

NgRx is particularly useful for applications with complex state requirements, such as:

  1. Applications with shared state across multiple components.
  2. Applications that require undo/redo functionality.
  3. Applications with asynchronous data fetching and caching.

Why Use NgRx?

Managing state in large applications can become challenging as the application grows. Without a proper state management solution, you may encounter issues like:

  1. Tight coupling between components.
  2. Difficulty in debugging due to scattered state logic.
  3. Inconsistent state across the application.

NgRx solves these problems by providing:

  1. Centralised State: All application state is stored in a single, immutable store.
  2. Predictability: State changes are predictable because they are handled by pure functions called reducers.
  3. Debugging Tools: NgRx integrates with tools like the Redux DevTools, making it easier to debug and track state changes.
  4. Reactive Programming: NgRx leverages RxJS to handle asynchronous operations and state updates in a reactive way.

Core Concepts of NgRx

Before diving into implementation, let’s understand the core building blocks of NgRx:

  1. Store: The store is a centralized container that holds the application state. It is immutable, meaning the state cannot be modified directly.
  2. Actions: Actions are plain objects that describe an event or intention to change the state. They are dispatched to the store to trigger state changes.
  3. Reducers: Reducers are pure functions that take the current state and an action as input and return a new state. They define how the state should change in response to an action.
  4. Selectors: Selectors are functions used to query and retrieve specific pieces of state from the store.
  5. Effects: Effects handle side effects, such as API calls or other asynchronous operations, and dispatch new actions based on the results.

Setting Up NgRx in an Angular Application

Let’s walk through the steps to set up and use NgRx in an Angular application.

Step 1: Install NgRx

To get started, install the required NgRx packages:

npm install @ngrx/store @ngrx/effects @ngrx/store-devtools @ngrx/entity @ngrx/router-store

Step 2: Define the State

Create an interface to define the shape of your application state. For example, if you’re managing a list of products:

export interface Product {
id: number;
name: string;
price: number;
}
export interface AppState {
products: Product[];
}

Step 3: Create Actions

Actions represent events that can change the state. Define actions for adding, removing, or loading products:

import { createAction, props } from '@ngrx/store';
import { Product } from './product.model';
export const loadProducts = createAction('[Product] Load Products');
export const loadProductsSuccess = createAction(
'[Product] Load Products Success',
props<{ products: Product[] }>()
);
export const addProduct = createAction(
'[Product] Add Product',
props<{ product: Product }>()
);
export const removeProduct = createAction(
'[Product] Remove Product',
props<{ productId: number }>()
);

Step 4: Create a Reducer

Reducers handle state changes based on the dispatched actions. Use the createReducer function to define how the state should change:

import { createReducer, on } from '@ngrx/store';
import { Product } from './product.model';
import { addProduct, removeProduct, loadProductsSuccess } from './product.actions';
export const initialState: Product[] = [];export const productReducer = createReducer(
initialState,
on(loadProductsSuccess, (state, { products }) => [...products]),
on(addProduct, (state, { product }) => [...state, product]),
on(removeProduct, (state, { productId }) =>
state.filter(product => product.id !== productId)
)
);

Step 5: Register the Store

Register the reducer in the AppModule using the StoreModule.forRoot method:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { productReducer } from './state/product.reducer';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ products: productReducer })
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {}

Step 6: Create Selectors

Selectors are used to retrieve specific pieces of state from the store. Define selectors for the product state:

import { createSelector, createFeatureSelector } from '@ngrx/store';
import { Product } from './product.model';
export const selectProducts = createFeatureSelector<Product[]>('products');export const selectProductById = (productId: number) =>
createSelector(selectProducts, products =>
products.find(product => product.id === productId)
);

Step 7: Use the Store in Components

Inject the Store service into your components to dispatch actions and select state:

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Product } from './product.model';
import { addProduct, loadProducts } from './state/product.actions';
import { selectProducts } from './state/product.selectors';
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of products\$ | async">
{{ product.name }} - \${{ product.price }}
</div>
<button (click)="addNewProduct()">Add Product</button>
`
})
export class ProductListComponent implements OnInit {
products\$: Observable<Product[]>;
constructor(private store: Store) {
this.products\$ = this.store.select(selectProducts);
}
ngOnInit() {
this.store.dispatch(loadProducts());
}
addNewProduct() {
const newProduct: Product = { id: 3, name: 'New Product', price: 100 };
this.store.dispatch(addProduct({ product: newProduct }));
}
}

Step 8: Handle Side Effects with Effects

Use NgRx Effects to handle side effects like API calls. For example, fetching products from an API:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { ProductService } from './product.service';
import { loadProducts, loadProductsSuccess } from './state/product.actions';
@Injectable()
export class ProductEffects {
loadProducts\$ = createEffect(() =>
this.actions\$.pipe(
ofType(loadProducts),
mergeMap(() =>
this.productService.getProducts().pipe(
map(products => loadProductsSuccess({ products })),
catchError(() => of({ type: '[Product] Load Products Failure' }))
)
)
)
);
constructor(private actions\$: Actions, private productService: ProductService) {}
}

Register the ProductEffects in the AppModule:

import { EffectsModule } from '@ngrx/effects';
import { ProductEffects } from './state/product.effects';
@NgModule({
imports: [
EffectsModule.forRoot([ProductEffects])
]
})
export class AppModule {}

Conclusion

NgRx provides a powerful and scalable solution for managing state in Angular applications. By following the principles of unidirectional data flow and immutability, NgRx makes it easier to build predictable and maintainable applications. While the initial setup may seem complex, the benefits of centralized state management, debugging tools, and reactive programming make it worth the effort.

--

--

Rahul Shrivastava
Rahul Shrivastava

Written by Rahul Shrivastava

I have 10 years of front-end development experience with AngularJS, Angular, ReactJS, and VueJS, and expertise in microfrontend architecture.

No responses yet