Home Web Front-end JS Tutorial Mastering Angular State Management using NgRx

Mastering Angular State Management using NgRx

Sep 10, 2024 pm 04:30 PM

State management in Angular ensures that data is consistently and efficiently shared across all parts of an application. Instead of each component managing its own data, a central store holds the state.

This centralization ensures that when data changes, all components automatically reflect the updated state, leading to consistent behavior and simpler code. It also makes the app easier to maintain and scale, as data flow is managed from a single source of truth.

In this article, we’ll explore how to implement state management in Angular using NgRx by building a simple shopping cart application. We’ll cover the core concepts of NgRx, such as the Store, Actions, Reducers, Selectors, and Effects, and demonstrate how these pieces fit together to manage the state of your application effectively.

State in Angular refers to the data your app needs to manage and display, like a shopping cart’s contents.

Why you need State Management

1. Consistency: It ensures that data is uniform across all components. When data changes in one place, the central store updates all relevant components automatically, preventing inconsistencies.

2. Simplified Data Flow: Instead of passing data between components manually, state management allows any component to access or update data directly from the central store, making the app’s data flow easier to manage and understand.

3. Easier Maintenance and Scalability: By centralizing data management, state management reduces code duplication and complexity. This makes the app easier to maintain, debug, and scale as it grows.

4. Performance Optimization: State management solutions often come with tools to optimize performance, such as selectively updating only the components that need to react to a change in state, rather than re-rendering the entire application.

How NgRx works

NgRx is a state management library for Angular that helps manage and maintain the state of your application in a predictable way.

Mastering Angular State Management using NgRx

1. Component

The component is where the user interacts with your app. It might be a button to add an item to the shopping cart.

Components and services are separated and don’t communicate with each other directly, instead services are used within effects thus creating an application structure different from a traditional Angular app.

2. Action

An action describes what happened and contains any necessary payload (data).

3. Reducer

Updates the state based on the action.

4. Store

The store is a centralized place that holds the entire state of your application.

5. Selector

Extracts data from the store for components.

6. Effects

Effects are where you handle logic that doesn’t belong in the reducer, like API calls.

7. Service

Services perform the actual business logic or API calls. Effects often use services to perform tasks like fetching data from a server.

When to Use NgRx

Use NgRx when your app’s complexity justifies it, but for straightforward apps, stick to simpler state management methods. Angular’s services, signals and @Input/@Output bindings between components are usually sufficient for managing state in less complex applications.

Example: Building an Add to Cart Feature with NgRx

1.Create a New Angular Project:

ng new shopping-cart
Copy after login

2. Install NGRX and Effects
To install NGRX and Effects, run the following command in your terminal:

ng add @ngrx/store@latest

ng add @ngrx/effects
Copy after login

3. Define the Product Model
Inside the src/app directory, create a file named product.model.ts

Define the Product interface to represent the structure of a product:

export interface Product {
    id: string;
    name: string;
    price: number;
    quantity: number;
}
Copy after login

4. Set Up State Management
Step 1: Create state Folder inside the src/app directory

Step 2: Define Cart Actions

Create cart.actions.ts in the state folder.

import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Product } from '../product.model';

export const CartActions = createActionGroup({
  source: 'Cart',
  events: {
    'Add Product': props<{ product: Product }>(),
    'Remove Product': props<{ productId: string }>(),
    'Update Quantity': props<{ productId: string; quantity: number }>(),
    'Load Products': emptyProps,
  },
});

export const CartApiActions = createActionGroup({
  source: 'Cart API',
  events: {
    'Load Products Success': props<{ products: Product[] }>(),
    'Load Products Failure': props<{ error: string }>(),
  },
});
Copy after login

Step 3: Create Reducers

Create cart.reducer.ts in the state folder.

import { createReducer, on } from '@ngrx/store';
import { Product } from '../product.model';
import { CartActions, CartApiActions } from './cart.actions';

// Initial state for products and cart
export const initialProductsState: ReadonlyArray<Product> = [];
export const initialCartState: ReadonlyArray<Product> = [];

// Reducer for products (fetched from API)
export const productsReducer = createReducer(
  initialProductsState,
  on(CartApiActions.loadProductsSuccess, (_state, { products }) => products)
);

// Reducer for cart (initially empty)
export const cartReducer = createReducer(
  initialCartState,
  on(CartActions.addProduct, (state, { product }) => {
    const existingProduct = state.find(p => p.id === product.id);
    if (existingProduct) {
      return state.map(p =>
        p.id === product.id ? { ...p, quantity: p.quantity + product.quantity } : p
      );
    }
    return [...state, product];
  }),
  on(CartActions.removeProduct, (state, { productId }) =>
    state.filter(p => p.id !== productId)
  ),
  on(CartActions.updateQuantity, (state, { productId, quantity }) =>
    state.map(p =>
      p.id === productId ? { ...p, quantity } : p
    )
  )
);
Copy after login

Step 4: Create Selectors

In the state folder, create cart.selectors.ts

import { createSelector, createFeatureSelector } from '@ngrx/store';
import { Product } from '../product.model';

export const selectProducts = createFeatureSelector<ReadonlyArray<Product>>('products');

export const selectCart = createFeatureSelector<ReadonlyArray<Product>>('cart');

export const selectCartTotal = createSelector(selectCart, (cart) =>
  cart.reduce((total, product) => total + product.price * product.quantity, 0)
);
Copy after login

Step 5: Create Effects

Create a new file cart.effects.ts in the state folder that listens for the Load Products action, uses the service to fetch products, and dispatches either a success or failure action.

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { ProductService } from '../product.service';
import { CartActions, CartApiActions } from './cart.actions';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class CartEffects {
  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(CartActions.loadProducts),
      mergeMap(() =>
        this.productService.getProducts().pipe(
          map(products => CartApiActions.loadProductsSuccess({ products })),
          catchError(error => of(CartApiActions.loadProductsFailure({ error })))
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private productService: ProductService
  ) {}
}
Copy after login

5. Connect the State Management to Your App
In a file called app.config.ts, set up configurations for providing the store and effects to the application.

import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideHttpClient } from '@angular/common/http';
import { cartReducer, productsReducer } from './state/cart.reducer';
import { provideEffects } from '@ngrx/effects';
import { CartEffects } from './state/cart.effects';

export const appConfig: ApplicationConfig = {
  providers: [
    provideStore({
      products: productsReducer, 
      cart: cartReducer 
    }),
    provideHttpClient(),
    provideEffects([CartEffects])
],
};
Copy after login

6. Create a Service to Fetch Products
In the src/app directory create product.service.ts to implement the service to fetch products

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Product } from './product.model';

@Injectable({ providedIn: 'root' })
export class ProductService {
  getProducts(): Observable<Array<Product>> {
    return of([
      { id: '1', name: 'Product 1', price: 10, quantity: 1 },
      { id: '2', name: 'Product 2', price: 20, quantity: 1 },
    ]);
  }
}
Copy after login

7. Create the Product List Component
Run the following command to generate the component: ng generate component product-list

This component displays the list of products and allows adding them to the cart.

Modify the product-list.component.ts file:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Product } from '../product.model';
import { selectProducts } from '../state/cart.selectors';
import { CartActions } from '../state/cart.actions';

@Component({
  selector: 'app-product-list',
  standalone: true,
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css'],
  imports: [CommonModule],
})
export class ProductListComponent implements OnInit {
  products$!: Observable<ReadonlyArray<Product>>;

  constructor(private store: Store) {

  }

  ngOnInit(): void {
    this.store.dispatch(CartActions.loadProducts()); // Dispatch load products action
    this.products$ = this.store.select(selectProducts); // Select products from the store
  }

  onAddToCart(product: Product) {
    this.store.dispatch(CartActions.addProduct({ product }));
  }
}
Copy after login

Modify the product-list.component.html file:

<div *ngIf="products$ | async as products">
  <div class="product-item" *ngFor="let product of products">
    <p>{{product.name}}</p>
    <span>{{product.price | currency}}</span>
    <button (click)="onAddToCart(product)" data-test="add-button">Add to Cart</button>
  </div>
</div>
Copy after login

8. Create the Shopping Cart Component
Run the following command to generate the component: ng generate component shopping-cart

This component displays the products in the cart and allows updating the quantity or removing items from the cart.

Modify the shopping-cart.component.ts file:

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { Product } from '../product.model';
import { selectCart, selectCartTotal } from '../state/cart.selectors';
import { CartActions } from '../state/cart.actions';

@Component({
  selector: 'app-shopping-cart',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './shopping-cart.component.html',
  styleUrls: ['./shopping-cart.component.css'],
})
export class ShoppingCartComponent implements OnInit {
  cart$: Observable<ReadonlyArray<Product>>;
  cartTotal$: Observable<number>;

  constructor(private store: Store) {
    this.cart$ = this.store.select(selectCart);
    this.cartTotal$ = this.store.select(selectCartTotal);
  }

  ngOnInit(): void {}

  onRemoveFromCart(productId: string) {
    this.store.dispatch(CartActions.removeProduct({ productId }));
  }

  onQuantityChange(event: Event, productId: string) {
    const inputElement = event.target as HTMLInputElement;
    let quantity = parseInt(inputElement.value, 10);

    this.store.dispatch(CartActions.updateQuantity({ productId, quantity }));
  }
}
Copy after login

Modify the shopping-cart.component.html file:

<div *ngIf="cart$ | async as cart">
  <div class="cart-item" *ngFor="let product of cart">
    <p>{{product.name}}</p><span>{{product.price | currency}}</span>
    <input type="number" [value]="product.quantity" (input)="onQuantityChange($event, product.id)" />
    <button (click)="onRemoveFromCart(product.id)" data-test="remove-button">Remove</button>
  </div>
  <div class="total">
    Total: {{cartTotal$ | async | currency}}
  </div>
</div>
Copy after login

Modify the shopping-cart.component.css file:

.cart-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}

.cart-item p {
  margin: 0;
  font-size: 16px;
}

.cart-item input {
  width: 50px;
  text-align: center;
}

.total {
  font-weight: bold;
  margin-top: 20px;
}
Copy after login

9. Put Everything Together in the App Component
This component will display the product list and the shopping cart

Modify the app.component.ts file:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product-list/product-list.component';
import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component';
import { NgIf } from '@angular/common';

@Component({
  selector: 'app-root',
  standalone: true,
  templateUrl: './app.component.html',
  imports: [CommonModule, ProductListComponent, ShoppingCartComponent, NgIf],
})
export class AppComponent {}
Copy after login

Modify the app.component.html file:

<!-- app.component.html -->
<h2>Products</h2>
<app-product-list></app-product-list>

<h2>Shopping Cart</h2>
<app-shopping-cart></app-shopping-cart>
Copy after login

10. Running the Application
Finally, run your application using ng serve.

Now, you can add products to your cart, remove them, or update their quantities.

Conclusion

In this article, we built a simple shopping cart application to demonstrate the core concepts of NgRx, such as the Store, Actions, Reducers, Selectors, and Effects. This example serves as a foundation for understanding how NgRx works and how it can be applied to more complex applications.

As your Angular projects grow in complexity, leveraging NgRx for state management will help you maintain consistency across your application, reduce the likelihood of bugs, and make your codebase easier to maintain.

To get the code for the above project, click the link below:
https://github.com/anthony-kigotho/shopping-cart

The above is the detailed content of Mastering Angular State Management using NgRx. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1664
14
PHP Tutorial
1266
29
C# Tutorial
1239
24
Demystifying JavaScript: What It Does and Why It Matters Demystifying JavaScript: What It Does and Why It Matters Apr 09, 2025 am 12:07 AM

JavaScript is the cornerstone of modern web development, and its main functions include event-driven programming, dynamic content generation and asynchronous programming. 1) Event-driven programming allows web pages to change dynamically according to user operations. 2) Dynamic content generation allows page content to be adjusted according to conditions. 3) Asynchronous programming ensures that the user interface is not blocked. JavaScript is widely used in web interaction, single-page application and server-side development, greatly improving the flexibility of user experience and cross-platform development.

The Evolution of JavaScript: Current Trends and Future Prospects The Evolution of JavaScript: Current Trends and Future Prospects Apr 10, 2025 am 09:33 AM

The latest trends in JavaScript include the rise of TypeScript, the popularity of modern frameworks and libraries, and the application of WebAssembly. Future prospects cover more powerful type systems, the development of server-side JavaScript, the expansion of artificial intelligence and machine learning, and the potential of IoT and edge computing.

JavaScript Engines: Comparing Implementations JavaScript Engines: Comparing Implementations Apr 13, 2025 am 12:05 AM

Different JavaScript engines have different effects when parsing and executing JavaScript code, because the implementation principles and optimization strategies of each engine differ. 1. Lexical analysis: convert source code into lexical unit. 2. Grammar analysis: Generate an abstract syntax tree. 3. Optimization and compilation: Generate machine code through the JIT compiler. 4. Execute: Run the machine code. V8 engine optimizes through instant compilation and hidden class, SpiderMonkey uses a type inference system, resulting in different performance performance on the same code.

Python vs. JavaScript: The Learning Curve and Ease of Use Python vs. JavaScript: The Learning Curve and Ease of Use Apr 16, 2025 am 12:12 AM

Python is more suitable for beginners, with a smooth learning curve and concise syntax; JavaScript is suitable for front-end development, with a steep learning curve and flexible syntax. 1. Python syntax is intuitive and suitable for data science and back-end development. 2. JavaScript is flexible and widely used in front-end and server-side programming.

JavaScript: Exploring the Versatility of a Web Language JavaScript: Exploring the Versatility of a Web Language Apr 11, 2025 am 12:01 AM

JavaScript is the core language of modern web development and is widely used for its diversity and flexibility. 1) Front-end development: build dynamic web pages and single-page applications through DOM operations and modern frameworks (such as React, Vue.js, Angular). 2) Server-side development: Node.js uses a non-blocking I/O model to handle high concurrency and real-time applications. 3) Mobile and desktop application development: cross-platform development is realized through ReactNative and Electron to improve development efficiency.

How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration) How to Build a Multi-Tenant SaaS Application with Next.js (Frontend Integration) Apr 11, 2025 am 08:22 AM

This article demonstrates frontend integration with a backend secured by Permit, building a functional EdTech SaaS application using Next.js. The frontend fetches user permissions to control UI visibility and ensures API requests adhere to role-base

Building a Multi-Tenant SaaS Application with Next.js (Backend Integration) Building a Multi-Tenant SaaS Application with Next.js (Backend Integration) Apr 11, 2025 am 08:23 AM

I built a functional multi-tenant SaaS application (an EdTech app) with your everyday tech tool and you can do the same. First, what’s a multi-tenant SaaS application? Multi-tenant SaaS applications let you serve multiple customers from a sing

From C/C   to JavaScript: How It All Works From C/C to JavaScript: How It All Works Apr 14, 2025 am 12:05 AM

The shift from C/C to JavaScript requires adapting to dynamic typing, garbage collection and asynchronous programming. 1) C/C is a statically typed language that requires manual memory management, while JavaScript is dynamically typed and garbage collection is automatically processed. 2) C/C needs to be compiled into machine code, while JavaScript is an interpreted language. 3) JavaScript introduces concepts such as closures, prototype chains and Promise, which enhances flexibility and asynchronous programming capabilities.

See all articles