NgRx Effects is a powerful library that allows you to handle side effects in your Angular application. Side effects are operations that interact with the outside world, such as HTTP requests, logging, or other asynchronous operations. NgRx Effects helps you isolate these side effects from your components and state management logic, making your application more predictable and easier to test.

Key Concepts

  1. Effects: Functions that listen for specific actions dispatched to the store and perform side effects in response.
  2. Actions: Objects that represent an event or intention to change the state.
  3. Reducers: Functions that handle state transitions based on actions.
  4. Selectors: Functions that select a piece of state from the store.

Setting Up NgRx Effects

Step 1: Install NgRx Effects

First, you need to install the NgRx Effects library:

npm install @ngrx/effects

Step 2: Create an Effect

Create a new file for your effects, for example, auth.effects.ts:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { AuthService } from '../services/auth.service';
import { login, loginSuccess, loginFailure } from '../actions/auth.actions';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class AuthEffects {
  constructor(
    private actions$: Actions,
    private authService: AuthService
  ) {}

  login$ = createEffect(() =>
    this.actions$.pipe(
      ofType(login),
      mergeMap(action =>
        this.authService.login(action.credentials).pipe(
          map(user => loginSuccess({ user })),
          catchError(error => of(loginFailure({ error })))
        )
      )
    )
  );
}

Step 3: Register the Effect

Register the effect in your AppModule or a feature module:

import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { AuthEffects } from './effects/auth.effects';

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

Practical Example

Let's create a simple example where we handle user login using NgRx Effects.

Step 1: Define Actions

Create a file auth.actions.ts:

import { createAction, props } from '@ngrx/store';

export const login = createAction(
  '[Auth] Login',
  props<{ credentials: { username: string; password: string } }>()
);

export const loginSuccess = createAction(
  '[Auth] Login Success',
  props<{ user: any }>()
);

export const loginFailure = createAction(
  '[Auth] Login Failure',
  props<{ error: any }>()
);

Step 2: Create a Service

Create a file auth.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  constructor(private http: HttpClient) {}

  login(credentials: { username: string; password: string }): Observable<any> {
    return this.http.post('/api/login', credentials);
  }
}

Step 3: Create the Effect

Create a file auth.effects.ts:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { AuthService } from '../services/auth.service';
import { login, loginSuccess, loginFailure } from '../actions/auth.actions';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { of } from 'rxjs';

@Injectable()
export class AuthEffects {
  constructor(
    private actions$: Actions,
    private authService: AuthService
  ) {}

  login$ = createEffect(() =>
    this.actions$.pipe(
      ofType(login),
      mergeMap(action =>
        this.authService.login(action.credentials).pipe(
          map(user => loginSuccess({ user })),
          catchError(error => of(loginFailure({ error })))
        )
      )
    )
  );
}

Step 4: Register the Effect

Register the effect in your AppModule:

import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { AuthEffects } from './effects/auth.effects';

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

Practical Exercise

Exercise

  1. Create a new Angular service called ProductService that fetches a list of products from an API.
  2. Define actions for loading products: loadProducts, loadProductsSuccess, and loadProductsFailure.
  3. Create an effect that listens for the loadProducts action, calls the ProductService to fetch products, and dispatches either loadProductsSuccess or loadProductsFailure based on the result.

Solution

Step 1: Define Actions

Create a file product.actions.ts:

import { createAction, props } from '@ngrx/store';

export const loadProducts = createAction('[Product] Load Products');

export const loadProductsSuccess = createAction(
  '[Product] Load Products Success',
  props<{ products: any[] }>()
);

export const loadProductsFailure = createAction(
  '[Product] Load Products Failure',
  props<{ error: any }>()
);

Step 2: Create a Service

Create a file product.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  constructor(private http: HttpClient) {}

  getProducts(): Observable<any[]> {
    return this.http.get<any[]>('/api/products');
  }
}

Step 3: Create the Effect

Create a file product.effects.ts:

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

@Injectable()
export class ProductEffects {
  constructor(
    private actions$: Actions,
    private productService: ProductService
  ) {}

  loadProducts$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadProducts),
      mergeMap(() =>
        this.productService.getProducts().pipe(
          map(products => loadProductsSuccess({ products })),
          catchError(error => of(loadProductsFailure({ error })))
        )
      )
    )
  );
}

Step 4: Register the Effect

Register the effect in your AppModule:

import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { ProductEffects } from './effects/product.effects';

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

Conclusion

NgRx Effects is a powerful tool for handling side effects in your Angular application. By isolating side effects from your components and state management logic, you can create more predictable and testable applications. In this section, you learned how to set up NgRx Effects, create effects, and handle side effects in a practical example. You also completed an exercise to reinforce your understanding of NgRx Effects. In the next module, we will explore state management in more detail.

© Copyright 2024. All rights reserved