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.
6
