NgRx/Effects Example

By Arvind Rai, 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.

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 
2. To install @ngrx/store, run following command.
npm install @ngrx/store --save 
3. In our application we will use angular-in-memory-web-api to fetch JSON data to test application. To install it, run following command.
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) 
sourceFunction : A function which returns an 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

The EffectsModule 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 { } 
Using EffectsModule.forFeature
import { EffectsModule } from '@ngrx/effects';

@NgModule({
  imports: [
    ------
    EffectsModule.forFeature([ CompanyEffects, EmployeeEffects ])
  ]
  ------
})
export class FeatureModule {} 

Effects with RxJS switchMap

RxJS switchMap 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}))
    )
  )
)); 
Here we have created 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 RxJS mergeMap 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}))
    )
  )
)); 
We have created an 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

RxJS catchError 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 any Action 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}); 
In the above code, the Effect will not dispatch 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 };
  }
}  
We will get following Web Service URL.
/api/articles 
2. Import 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 
Now find the complete code.
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}))
      )
    )
  ));

} 
article.actions.ts
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'); 
article.service.ts
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);
    }    
} 
article.ts
export interface Article {
	id: number;
	title: string;
	category: string;
} 
app.states.ts
import { Article } from '../models/article';

export interface AppState {
	articleState: ArticleState;
}

export interface ArticleState {
	articles: Article[];
	message: any;
} 
article.reducer.ts
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
); 
reducers.ts
import { ActionReducerMap } from '@ngrx/store';
import { AppState } from './app.states';
import * as fromReducer from './article.reducer';

export const reducers: ActionReducerMap<AppState> = {
  articleState: fromReducer.articleReducer
}; 
article.component.ts
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}));
	}
} 
article.component.html
<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> 
app.component.ts
import { Component } from '@angular/core';

@Component({
   selector: 'app-root',
   template: `
	<app-article></app-article>
     `
})
export class AppComponent {
}  
app.module.ts
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.
NgRx/Effects Example

References

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

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
LEARN MORE








©2024 concretepage.com | Privacy Policy | Contact Us