All Course > Angular > State Management In Angular Feb 16, 2025

NgRx State Management in Angular

In the previous lesson, we explored RxJS for state management, where we used Observables and Subjects to handle app-wide data. While RxJS works well, managing complex state in large apps can get messy. That’s where NgRx comes in—a Redux-based state management solution for Angular that brings structure and predictability.

In this lesson, I’ll walk you through NgRx, which follows the Redux pattern with actions, reducers, effects, and store. I’ve faced issues with scattered state in big apps, and NgRx helped me keep everything in one place. Let’s see how it works.

Why NgRx? A Real-World Use Case

Recently, I built an e-commerce app where users could add items to a cart, apply discounts, and track orders. At first, I used simple services with RxJS, but as features grew, tracking state changes became hard. Bugs popped up when multiple components changed the same data.

NgRx solved this by enforcing a single source of truth. The store held the entire state, actions described changes, reducers updated state predictably, and effects handled side effects like API calls. Debugging became easier since every state change was logged.

Now, let’s implement NgRx step by step.

Setting Up NgRx in an Angular Project

First, install NgRx packages:

ng add @ngrx/store @ngrx/effects @ngrx/store-devtools

This adds @ngrx/store (core state management), @ngrx/effects (side effects handling), and @ngrx/store-devtools (debugging). Then, import StoreModule and EffectsModule in app.module.ts and initialize them:

  • @ngrx/store (core Redux store)

  • @ngrx/effects (handles side effects)

  • @ngrx/store-devtools (debugging in Chrome)

import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';

@NgModule({
  imports: [
    StoreModule.forRoot({}),
    EffectsModule.forRoot([]),
  ]
})
export class AppModule { }

This snippet sets up NgRx Store and Effects in an Angular app. StoreModule.forRoot({}) initializes the global state container, while EffectsModule.forRoot([]) enables side-effect handling. The empty objects ({} and []) are placeholders—later, you’ll add reducers and effects here. This configuration is added to the AppModule imports to enable NgRx across the app.

Defining Actions

Actions in NgRx describe events that change state, like adding an item to a cart or fetching data. Each action has a type (e.g., [Cart] Add Item) and can carry a payload (e.g., product details). They act as instructions for reducers, telling them how to update the store.

// cart.actions.ts
import { createAction, props } from '@ngrx/store';

export const addToCart = createAction(
  '[Cart] Add Item',
  props<{ item: string }>()
);

export const removeFromCart = createAction(
  '[Cart] Remove Item',
  props<{ itemId: string }>()
);

This code defines NgRx actions for a shopping cart. The addToCart action takes an item (string) as payload, while removeFromCart uses an itemId. These actions describe state changes, like adding/removing items, which reducers later process to update the store. The [Cart] prefix helps debug actions in DevTools.

Creating a Reducer

A reducer in NgRx is a pure function that takes the current state and an action, then returns a new state without modifying the original. It determines how the state should change based on the action type. For example, if the action is addToCart, the reducer adds the item to the cart array. Reducers must be immutable, meaning they return a new state object instead of modifying the existing one. This ensures predictable state changes and makes debugging easier.

// cart.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { addToCart, removeFromCart } from './cart.actions';

export interface CartState {
  items: string[];
}

const initialState: CartState = {
  items: []
};

export const cartReducer = createReducer(
  initialState,
  on(addToCart, (state, { item }) => ({
    ...state,
    items: [...state.items, item]
  })),
  on(removeFromCart, (state, { itemId }) => ({
    ...state,
    items: state.items.filter(id => id !== itemId)
  }))
);

This code sets up a reducer for a shopping cart using NgRx. It defines a CartState interface with an items array and an initialState with an empty cart. The cartReducer uses createReducer to handle two actions: addToCart (which appends an item to the array) and removeFromCart (which filters out an item by ID). Each action returns a new state without mutating the old one, following Redux principles.

Now, register the reducer in app.module.ts:

StoreModule.forRoot({ cart: cartReducer })

The StoreModule.forRoot({ cart: cartReducer }) sets up the NgRx store at the root level of your Angular app, where cart is the state key and cartReducer handles state updates for the cart. This makes the store globally available, letting components access and modify state predictably.

Using the Store in Components

To use the NgRx Store in components, inject the Store service and select the state you need. For example, if you have a cart state, you can access it using this.store.select(state => state.cart.items). To update the state, dispatch actions like this.store.dispatch(addToCart({ item: ‘Product1’ })). This keeps your component clean by moving state logic to the store, making it easier to manage and test. Observables like items$ automatically update the UI when the state changes.

// cart.component.ts
import { Store } from '@ngrx/store';
import { addToCart } from './cart.actions';

@Component({
  selector: 'app-cart',
  template: `
    <button (click)="addItem('Product1')">Add to Cart</button>
    <div *ngFor="let item of items$ | async">{{ item }}</div>
  `
})
export class CartComponent {
  items$ = this.store.select(state => state.cart.items);

  constructor(private store: Store) {}

  addItem(item: string) {
    this.store.dispatch(addToCart({ item }));
  }
}

The CartComponent uses NgRx to manage cart items. It selects the items array from the store using items$, which updates automatically when the state changes. The addItem method dispatches an addToCart action, updating the store with the new item. This keeps the UI in sync with the app’s state.

Handling Side Effects with NgRx Effects

NgRx Effects help manage side effects like API calls, timers, or logging without cluttering components. Instead of calling services directly, Effects listen for actions, perform async tasks, and dispatch new actions with results. For example, when a [Cart] Load Items action triggers, an Effect can fetch data from an API, then dispatch a [Cart] Load Success action with the response. This keeps components clean and state updates predictable. To use Effects, define them as injectable classes, register them in your module, and let NgRx handle the rest.

// cart.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { mergeMap, map } from 'rxjs/operators';
import { ApiService } from './api.service';

@Injectable()
export class CartEffects {
  loadCartItems$ = createEffect(() =>
    this.actions$.pipe(
      ofType('[Cart] Load Items'),
      mergeMap(() => this.api.getCartItems().pipe(
        map(items => ({ type: '[Cart] Load Success', items }))
      ))
    )
  );

  constructor(private actions$: Actions, private api: ApiService) {}
}

This NgRx Effect listens for the [Cart] Load Items action. When triggered, it calls an API service (api.getCartItems()), then maps the response into a new action ([Cart] Load Success) with the fetched items. The mergeMap operator handles the async operation, ensuring smooth data flow. Effects keep side effects (like HTTP calls) separate from reducers, making state changes predictable.

EffectsModule.forRoot([CartEffects])

The EffectsModule.forRoot([CartEffects]) sets up NgRx effects at the root level, enabling side-effect handling (like API calls) in your Angular app. By registering CartEffects, any action that triggers an API request or async task will be processed automatically, keeping state changes clean and predictable. This is crucial for managing real-world app logic without cluttering components.

Conclusion

NgRx makes state management predictable and scalable. By using actions, reducers, and effects, you keep logic organized and avoid bugs. In the next lesson, we’ll cover Installing and Using Angular Material, which helps in building beautiful UIs. If you missed the last lesson, check out Using RxJS for State Management to see how NgRx improves upon RxJS.

Comments

There are no comments yet.

Write a comment

You can use the Markdown syntax to format your comment.

Tags: angular typescript