NgRx/Effects Example
December 20, 2021
This page will walk through NgRx/Effects example.
1. Effects powered by RxJS handle side effects for Store.
2. Effects are used to handle network requests, web socket messages and time-based events. Effects are useful for fetching data and long-running tasks.
3. In our Angular application, generally our components interact directly to services. It is better to isolate them using
Effect
.
4. Effects isolate side effects from components and enable them to select state and dispatch actions.
5. Effects are created in a service class and listen to
Observable
of every action dispatched from the Store.
6. Effects are selected to run on the basis of dispatched action.
7. Effects perform synchronous or asynchronous tasks and return new
Action
.
Contents
Technologies Used
Find the technologies being used in our example.1. Angular 13.0.0
2. NgRx 13.0.0
3. Node.js 12.20.0
4. NPM 8.2.0
Install NgRx/Effects
We will install Store and Effects in our Angular application as following.1. To install
@ngrx/effects
, run following command.
npm install @ngrx/effects --save
@ngrx/store
, run following command.
npm install @ngrx/store --save
npm install angular-in-memory-web-api@0.13.0 --save
Create Effects
1. Effects are created in Angular service class decorated with@Injectable
.
2. Inject NgRx
Actions
in service class.
3. To create Effect, NgRx provides
createEffect
function.
createEffect(sourceFunction, config)
Observable
.
config : Optional. It is a
Partial<EffectConfig>
to configure effect. The EffectConfig
has following default value.
{dispatch: true, useEffectsErrorHandler: true}
4. Actions are filtered using pipeable
ofType
operator of NgRx/Effects.
5. Effect services are registered using
EffectsModule
in application module.
Find the code to create Effect.
@Injectable() export class ArticleEffects { constructor( private actions$: Actions, private articleService: ArticleService ) { } loadAllArticles$ = createEffect(() => this.actions$.pipe( ofType(fromActions.ShowAllAction), switchMap(() => this.articleService.getAllArticles().pipe( map(data => fromActions.ShowAllSuccessAction({payload: data})) ) ) )); }
Register Effect Service using EffectsModule
TheEffectsModule
is the module for @ngrx/effects
library. To register Effect
services in application, we need to use EffectsModule.forRoot
in application module and in case of feature module we need to use EffectsModule.forFeature
.
Find the code.
Using EffectsModule.forRoot
import { EffectsModule } from '@ngrx/effects'; @NgModule({ imports: [ ------ EffectsModule.forRoot([ ArticleEffects, BookEffects ]) ] ------ }) export class AppModule { }
import { EffectsModule } from '@ngrx/effects'; @NgModule({ imports: [ ------ EffectsModule.forFeature([ CompanyEffects, EmployeeEffects ]) ] ------ }) export class FeatureModule {}
Effects with RxJS switchMap
RxJSswitchMap
maps the latest emitted Observable
result for the case when an Observable
emits multiple Observables
.
Find the code.
searchArticleById$ = createEffect(() => this.actions$.pipe( ofType(fromActions.GetByIdAction), debounceTime(500), map(action => action.payload), switchMap(id => this.articleService.getArticleById(id).pipe( map(res => fromActions.GetByIdSuccessAction({payload: res})) ) ) ));
searchArticleById$
Effect that accepts GetByIdAction
on dispatch. We are using debounceTime
as 500 milliseconds to generate a delay. The payload of incoming action is passed to service to handle HTTP request. After successful response we are dispatching new action that is GetByIdSuccessAction
and passing the response value as payload to this new action. This new action will be handled by Store.
Effects with RxJS mergeMap
The RxJSmergeMap
maps values by combining multiple Observables
into one by merging their emissions.
Find the sample code to use Effect with RxJS
mergeMap
operator.
createArticle$ = createEffect(() => this.actions$.pipe( ofType(fromActions.CreateAction), map(action => action.payload), mergeMap(article => this.articleService.createArticle(article).pipe( map(res => fromActions.CreateSuccessAction({payload: res})) ) ) ));
createArticle$
Effect that accepts CreateAction
on dispatch. The mergeMap
operator gets the payload. We are passing this payload to the service in which we are handling HTTP requests. After getting successful result we are dispatching new action that is CreateSuccessAction
and passing the result into it as payload of this new action. Store will handle this new dispatched action.
Effects with RxJS catchError
RxJScatchError
operator catches the error thrown by Observable
and handles it by returning a new Observable
or throwing user defined error. To convert a value into Observable
we can use RxJS of
.
Find the code.
createArticle$ = createEffect(() => this.actions$.pipe( ofType(fromActions.CreateAction), map(action => action.payload), mergeMap(article => this.articleService.createArticle(article).pipe( map(res => fromActions.CreateSuccessAction({payload: res})), catchError(error => of(fromActions.CreateFailureAction({payload: error}))) ) ) ));
Non-dispatching Effects
We can create a non-dispatching Effect which means the Effect will not dispatch anyAction
after processing. To create a non-dispatching Effect, pass a new parameter to createEffect
method as {dispatch: false}
.
loadAllArticles$ = createEffect(() => this.actions$.pipe( ofType(fromActions.ShowAllAction), switchMap(() => this.articleService.getAllArticles().pipe( map(data => fromActions.ShowAllSuccessAction({payload: data})) ) ) ), {dispatch: false});
ShowAllSuccessAction
at all because we have made this Effect as non-dispatching Effect.
Angular In-Memory Web API
Find the steps to use Angular In-Memory Web API.1. Create a class implementing
InMemoryDbService
. Define createDb()
method with some dummy data.
test-data.ts
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class TestData implements InMemoryDbService { createDb() { let articleDetails = [ {id: '1', title: 'Core Java Tutorial', category: 'Java'}, {id: '2', title: 'Angular Tutorial', category: 'Angular'}, {id: '3', title: 'Hibernate Tutorial', category: 'Hibernate'} ]; return { articles: articleDetails }; } }
/api/articles
InMemoryWebApiModule
in application module and configure TestData
class as following.
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { TestData } from './test-data'; @NgModule({ imports: [ ------ InMemoryWebApiModule.forRoot(TestData) ], ------ }) export class AppModule { }
Complete Example
Find the project structure of our application.my-app | |--src | | | |--app | | | | | |--actions | | | | | | | |--article.actions.ts | | | | | |--effects | | | | | | | |--article.effects.ts | | | | | |--services | | | | | | | |--article.service.ts | | | | | |--components | | | | | | | |--article.component.html | | | |--article.component.ts | | | | | |--models | | | | | | | |--article.ts | | | | | |--reducers | | | | | | | |--app.states.ts | | | |--article.reducer.ts | | | |--reducers.ts | | | | | |--app.component.ts | | |--app.module.ts | | |--test-data.ts | | | |--index.html | |--styles.css | |--package.json
article.effects.ts
import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { map, switchMap, mergeMap, catchError, debounceTime } from 'rxjs/operators'; import * as fromActions from '../actions/article.actions'; import { ArticleService } from '../services/article.service'; @Injectable() export class ArticleEffects { constructor( private actions$: Actions, private articleService: ArticleService ) { } loadAllArticles$ = createEffect(() => this.actions$.pipe( ofType(fromActions.ShowAllAction), switchMap(() => this.articleService.getAllArticles().pipe( map(data => fromActions.ShowAllSuccessAction({payload: data})) ) ) )); createArticle$ = createEffect(() => this.actions$.pipe( ofType(fromActions.CreateAction), map(action => action.payload), mergeMap(article => this.articleService.createArticle(article).pipe( map(res => fromActions.CreateSuccessAction({payload: res})), catchError(error => of(fromActions.CreateFailureAction({payload: error}))) ) ) )); searchArticleById$ = createEffect(() => this.actions$.pipe( ofType(fromActions.GetByIdAction), debounceTime(500), map(action => action.payload), switchMap(id => this.articleService.getArticleById(id).pipe( map(res => fromActions.GetByIdSuccessAction({payload: res})) ) ) )); }
import { createAction, props } from '@ngrx/store'; import { Article } from '../models/article'; export const ShowAllAction = createAction('[ARTICLE] Show All'); export const ShowAllSuccessAction = createAction('[ARTICLE] Show All Success', props<{ payload: Article[]}>()); export const CreateAction = createAction('[ARTICLE] Create', props<{ payload: Article}>()); export const CreateSuccessAction = createAction('[ARTICLE] Create Success', props<{ payload: Article}>()); export const CreateFailureAction = createAction('[ARTICLE] Create Failure', props<{ payload: any}>()); export const GetByIdAction = createAction('[ARTICLE] Get by Id', props<{ payload: string}>()); export const GetByIdSuccessAction = createAction('[ARTICLE] Get by Id Success', props<{ payload: Article[]}>()); export const ResetAction = createAction('[ARTICLE] Reset');
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Article } from '../models/article'; @Injectable() export class ArticleService { constructor(private http: HttpClient) { } url = "/api/articles"; getAllArticles(): Observable<Article[]> { return this.http.get<Article[]>(this.url); } createArticle(article: Article): Observable<Article> { return this.http.post<Article>(this.url, article); } getArticleById(id: string): Observable<Article[]> { console.log(id); return this.http.get<Article[]>(this.url + '?id=' + id); } }
export interface Article { id: number; title: string; category: string; }
import { Article } from '../models/article'; export interface AppState { articleState: ArticleState; } export interface ArticleState { articles: Article[]; message: any; }
import { createFeatureSelector, createSelector, createReducer, on, Action } from '@ngrx/store'; import * as fromActions from '../actions/article.actions'; import { ArticleState } from './app.states'; export const initialState: ArticleState = {articles: [], message: ''}; // Creating reducer const _articleReducer = createReducer( initialState, on(fromActions.ShowAllSuccessAction, (state, {payload}) => ({articles: payload, message: 'Success'})), on(fromActions.CreateSuccessAction, (state, {payload}) => ({articles: [payload], message: 'Article Created.'})), on(fromActions.CreateFailureAction, (state, {payload}) => ({articles: [], message: payload})), on(fromActions.GetByIdSuccessAction, (state, {payload}) => ({articles: payload, message: 'Success'})), on(fromActions.ResetAction, (state) => ({ articles: [], message: ''})) ); export function articleReducer(state: any, action: Action) { return _articleReducer(state, action); } // Creating selectors export const getArticleState = createFeatureSelector<ArticleState>('articleState'); export const getArticles = createSelector( getArticleState, (state: ArticleState) => state.articles ); export const getMessage = createSelector( getArticleState, (state: ArticleState) => state.message );
import { ActionReducerMap } from '@ngrx/store'; import { AppState } from './app.states'; import * as fromReducer from './article.reducer'; export const reducers: ActionReducerMap<AppState> = { articleState: fromReducer.articleReducer };
import { FormBuilder, Validators } from '@angular/forms' import { Store } from '@ngrx/store'; import { Component } from '@angular/core'; import { Observable } from 'rxjs'; import * as fromReducer from '../reducers/article.reducer'; import * as fromActions from '../actions/article.actions'; import { ArticleState } from '../reducers/app.states'; import { Article } from '../models/article'; @Component({ selector: 'app-article', templateUrl: 'article.component.html' }) export class ArticleComponent { articles$: Observable<Article[]>; message$: Observable<any>; task= ''; constructor( private formBuilder:FormBuilder, private store: Store<ArticleState>) { this.articles$ = store.select(fromReducer.getArticles); this.message$ = store.select(fromReducer.getMessage); } articleForm = this.formBuilder.group({ id: ['', Validators.required ], title: '', category: '' }); get id() { return this.articleForm.get('id'); } onFormSubmit() { if(this.articleForm.valid) { let article = this.articleForm.value; this.createArticle(article); this.articleForm.reset(); } } createArticleView(){ this.task = 'create'; this.store.dispatch(fromActions.ResetAction()); } getArticleByIdView(){ this.task = 'get'; this.store.dispatch(fromActions.ResetAction()); } loadAllArticles(){ this.task = 'all'; this.store.dispatch(fromActions.ShowAllAction()); } createArticle(article: Article){ this.store.dispatch(fromActions.CreateAction({payload: article})); } searchArticleById(articleId: string){ this.store.dispatch(fromActions.GetByIdAction({payload: articleId})); } }
<button (click)="loadAllArticles()">Show All Articles</button> <button (click)="createArticleView()">Create Article</button> <button (click)="getArticleByIdView()">Search Article By Id</button> <div [ngSwitch]="task"> <ng-template [ngSwitchCase]="'all'"> <ul> <li *ngFor="let article of articles$ | async"> {{article.id}} - {{article.title}} - {{article.category}} </li> </ul> </ng-template> <ng-template ngSwitchCase="create"> <p> <b>{{ message$ | async }}</b> </p> <form [formGroup]="articleForm" (ngSubmit)="onFormSubmit()"> <p> Enter New Id: <input formControlName="id"> <label *ngIf="id?.hasError('required') && !articleForm.pristine" style="color:red"> Enter article id </label> </p> <p> Enter Title: <input formControlName="title"> </p> <p> Enter Category: <input formControlName="category"> </p> <p> <button> Submit </button> </p> </form> <ul> <li *ngFor="let article of articles$ | async"> {{article.id}} - {{article.title}} - {{article.category}} </li> </ul> </ng-template> <ng-template ngSwitchCase="get"> <p> Enter Id: <input ngModel (input)="searchArticleById(id.value)" #id="ngModel"> </p> <ul> <li *ngFor="let article of articles$ | async"> {{article.id}} - {{article.title}} - {{article.category}} </li> </ul> </ng-template> <ng-template ngSwitchDefault> <br /><b>Select Task</b> </ng-template> </div>
import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <app-article></app-article> ` }) export class AppComponent { }
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { AppComponent } from './app.component'; import { ArticleComponent } from './components/article.component'; import { reducers } from './reducers/reducers'; import { ArticleEffects } from './effects/article.effects'; import { ArticleService } from './services/article.service'; //For InMemory testing import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { TestData } from './test-data'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule, FormsModule, HttpClientModule, StoreModule.forRoot(reducers), EffectsModule.forRoot([ArticleEffects]), InMemoryWebApiModule.forRoot(TestData) ], declarations: [ AppComponent, ArticleComponent ], providers: [ ArticleService ], bootstrap: [ AppComponent ] }) export class AppModule { }
Run Application
To run the application, find the following steps.1. Download source code using download link given below on this page.
2. Use downloaded src in your Angular CLI application. To install Angular CLI, find the link.
3. Install NgRx/Store and NgRx/Effects and angular-in-memory-web-api using NPM command.
4. Run ng serve using command prompt.
5. Now access the URL http://localhost:4200
Find the print screen of output.

References
@ngrx/effects@ngrx/store
Getting started with Angular
angular-in-memory-web-api