Setting up the effects

Remember that reducers are pure functions so we cannot use our asynchronous service there, so we move to our app.effects.ts file to create our effect

The effect file initially looks like a service file in terms of structure.

It has an Injectable decorator but a different naming convention

the magic man is the injected action$ of type Action from @ngrx/effects, which is injected in the class constructor

We can also inject our service too in the constructor

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

We create our effect cases (Here your knowledge of RXJS comes into play)

Taking one case as an example, we import all our actions as a variable from the actions file

 //get single product
  getProduct$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.getProduct),
      mergeMap(({ id }) =>
        this.productService.getProduct(id).pipe(
          map(
            (product) => AppActions.getProductSuccess({ product }),
            catchError((error) => of(AppActions.getProductFail({ error })))
          )
        )
      )
    );
  });

The createEffect takes a source (a function that returns an observable) and config options as parameters

the ofType method takes our action as a parameter, then we return our observable by using our rxjs operators

The most common operator is mergeMap because it is optimal for all cases but it really depends on the situation, sometimes switchMap is better. To know more about the operators, take a course on RXJS

What mergeMap does is that it actually handles multiple observables by merging them, meaning when multiple instances of the same asynchronous call are made, it does not need to make a new observable but if one has been completed, it adds the observable to the next value.

We return the service methods (which should be returning an observable), pipe the result, and map the observer into our NGRX successful Action (which will be called), in case there is a property to be called (most likely from the observer), pass the parameter into the Action call

Pass the catchError function as the second Map parameter to handle the failure Action case.
The catchError takes in a parameter that is a callback that returns an observable of failure Action.
Remember that this callback is within the catchError scope and hence will not automatically return an Observable, so we use the rxjs of() operator to convert the Action to an observable

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, mergeMap, of, tap } from 'rxjs';
import { ProductService } from '../product.service';
import * as AppActions from './app.actions';
import { Router } from '@angular/router';

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

  //get single product
  getProduct$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.getProduct),
      mergeMap(({ id }) =>
        this.productService.getProduct(id).pipe(
          map(
            (product) => AppActions.getProductSuccess({ product }),
            catchError((error) => of(AppActions.getProductFail({ error })))
          )
        )
      )
    );
  });

  //get all products
  getProducts$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.getProducts),
      mergeMap(() =>
        this.productService.getProducts().pipe(
          map(
            (products) => AppActions.getProductsSuccess({ products }),
            catchError((error) => of(AppActions.getProductsFail({ error })))
          )
        )
      )
    );
  });

  //add product
  addProduct$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.AddProduct),
      mergeMap(({ name, price }) =>
        this.productService.addProduct({ name, price }).pipe(
          map(
            (product) => AppActions.AddProductSuccess({ product }),
            catchError((error) => of(AppActions.AddProductFail({ error })))
          )
        )
      )
    );
  });

  //If adding was successfull go back home
  addSuccess$ = createEffect(
    () => {
      return this.actions$.pipe(
        ofType(AppActions.AddProductSuccess),
        tap(() => this.router.navigateByUrl(''))
      );
    },
    { dispatch: false }
  );

  //Update product
  updateProduct$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.updateProduct),
      mergeMap(({ product }) =>
        this.productService.updateProduct(product).pipe(
          map(
            (product) => AppActions.updateProductSuccess({ product }),
            catchError((error) => of(AppActions.updateProductFail({ error })))
          )
        )
      )
    );
  });

  //If update was sucessfult go back home
  updateSuccess$ = createEffect(
    () => {
      return this.actions$.pipe(
        ofType(AppActions.updateProductSuccess),
        tap(() => this.router.navigateByUrl(''))
      );
    },
    { dispatch: false }
  );

  //delete product
  deleteProduct$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AppActions.deleteProduct),
      mergeMap(({ product }) =>
        this.productService.deleteProduct(product).pipe(
          map(
            () => AppActions.deleteProductSuccess({ product }),
            catchError((error) => of(AppActions.deleteProductFail({ error })))
          )
        )
      )
    );
  });
}

In summary, the createEffects must return an observable

We can build effects for all our asynchronous action cases.

Discussion

6

0