Home  >  Angular  >  NgRx

NgRx/Effects 4 Example

By Arvind Rai, December 17, 2017
This page will walk through NgRx/Effects 4 example. An Effect listens for actions dispatched from Store, performs some operations and then dispatches new Action to Store or navigate to new path etc. Effect isolates side effects from components and provide new source of actions. NgRx provides @ngrx/effects library to work with Effect. It has @Effect() decorator and Actions class to create Effects. To work with Effect we need to create a service decorated with @Injectable() and then inject Actions into service. Now create property of Observable type decorated with @Effect(). The effect will configure actions by using ofType operator from Actions class. The Effect services will be configured in application module using EffectsModule. We can create non-dispatching Effects by passing { dispatch: false } to @Effect decorator. By default, effects are merged and subscribed to the Store. We can control the lifecycle of resolved effects by implementing OnRunEffects. @ngrx/effects provides utility mergeEffects to manually merge all decorated effects into a combined Observable. Here on this page we will create an application in which actions will be dispatched to Effects and then some operation will be performed using HttpClient and then new action will be dispatched by Effect to Store. Find the complete example step by step.

Technologies Used

Find the technologies being used in our example.
1. Angular 5.0.0
2. Angular CLI 1.5.4
3. NgRx/Store 4.1.1
4. NgRx/Effects 4.1.1
5. TypeScript 2.4.2
6. Node.js 6.11.0
7. NPM 3.10.10

Installing NgRx/Effects

We will install Store and Effects as following using NPM.
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 i angular-in-memory-web-api@0.5.3 --save 

EffectsModule

EffectsModule is the NgModule for @ngrx/effects library. To register @ngrx/effects services we need to use forRoot in root module and in case of feature module we need to use forFeature. Find the sample configuration.

forRoot
import { EffectsModule } from '@ngrx/effects';

@NgModule({
  imports: [
    ------
    EffectsModule.forRoot([ ArticleEffects, BookEffects ])
  ]
  ------
})
export class AppModule { } 
forFeature
import { EffectsModule } from '@ngrx/effects';

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

@Effect() and Actions

@Effect() decorator and Actions class are from @ngrx/effects library. @Effect() is used to create an Effect of Observable type to perform side effect. To create an Effect we will declare an Observable<Action> type property decorated with @Effect(). Now we will initialize this property to accept specific Actions and to dispatch new Action to Store.
Actions class is of Observable type. Actions represents an Observable of all actions dispatched to Store. Actions has ofType operator that is used to filter actions of certain type for which we want to perform side effect. The Effect will accept only those actions that is configured in ofType.
Effects are created within a service decorated with @Injectable(). We will inject Actions class into our service. Now find the sample code to create Effect.
import { Injectable } from '@angular/core';
import { Action } from '@ngrx/store';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';

@Injectable()
export class ArticleEffects {

  constructor(private actions$: Actions) {}      

  @Effect() 
  loadAllArticles$: Observable<Action> = this.actions$
    .ofType('CREATE', 'UPDATE')
    .do(action => {
       console.log(action);
    });
}
Look into the above code. We have created a class decorated with @Injectable(). First we have to inject Actions class to get the instance of it. The variable name of Observable type should end with $ such as actions$. Now we will create Effect. We need to create a property of Observable<Action> type. Now we will define what actions can it accept to perform side effect. To configure specific action type, we need to use ofType operator from Actions instance. Now to perform side effects we can use RxJS operators such as mergeMap, switchMap and exhaustMap etc which will emit a new action to store. In the above class we are using RxJS do operator where we can navigate using Router.navigate. In our above sample code we are just logging actions.

RxJS Operators

To work with NgRx, we should be aware with RxJS operators, functions and classes. Find some RxJS operators from the link.

mergeMap: Map values by combining multiple Observables into one by merging their emissions.
switchMap: When an Observable emits multiple Observables then the latest emitted Observable result is mapped by switchMap.
exhaustMap: It ignores every new projected Observable if the previous projected Observable has not yet completed.
do: Registers an action and can perform different Observable lifecycle events.
debounceTime: Only emit an item from an Observable if the specified time has passed without emitting another item.
map: It applies the given function to each item.
catch: It handles errors in an Observable sequence. We need to return Observable from catch function. We can use RxJS of function to convert a value into Observable.

The above RxJS operators can be imported as following.
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/exhaustMap';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch'; 

Effects with mergeMap: Emit new Action

Find the sample code to use Effect with RxJS mergeMap operator.
@Effect() 
createArticle$: Observable<Action> = this.actions$
 .ofType<fromActions.CreateAction>(fromActions.CREATE)
 .map(action => action.payload)
 .mergeMap(article => 
	 this.articleService.createArticle(article)
	 .map(res => new fromActions.CreateSuccessAction(res)) 
	 .catch(error => of(new fromActions.CreateFailureAction(error)))
 ); 
We have created an createArticle$ Effect decorated with @Effect() that will accept dispatched action of type CREATE. We have given generic action type to ofType that is CreateAction. It will help us to get the payload of the incoming action to this Effect. In the mergeMap operator we are passing this payload to a 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. If there is an error then we will dispatch an error action CreateFailureAction as Observable. To convert a value into Observable we can use RxJS function of that can be imported as following.
import { of } from 'rxjs/observable/of'; 

Effects with switchMap: Emit New Action

Find the sample code to use Effect with RxJS switchMap operator.
@Effect() 
searchArticleById$: Observable<Action> = this.actions$
  .ofType<fromActions.GetByIdAction>(fromActions.GET_BY_ID)
  .debounceTime(500)
  .map(action => action.payload)
  .switchMap(id => 
	 this.articleService.getArticleById(id)
	 .map(res => new fromActions.GetByIdSuccessAction(res)) 
  ); 
Here we have created searchArticleById$ Effect decorated with @Effect() that will accept dispatched action of the type GET_BY_ID. ofType is using generic type as GetByIdAction. We are using debounceTime as 500 milliseconds. The payload of incoming action is passed to service in which we are handling HTTP requests. 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.

Non-dispatching Effects using { dispatch: false }

We can prevent dispatching from an Effect by passing { dispatch: false } to @Effect decorator. Find the sample usage.
@Effect({ dispatch: false }) 
logActions$ = this.actions$
  .do(action => {
      console.log(action);
  }); 
If we have created an Effect to dispatch any action but changed it to { dispatch: false } then that Effect will not dispatch action anymore. Find the sample code.
@Effect({ dispatch: false })
loadAllArticles$: Observable<Action> = this.actions$
  .ofType(fromActions.SHOW_ALL)
  .switchMap(() => 
      this.articleService.getAllArticles()
      .map(data => new fromActions.ShowAllSuccessAction(data)) 
  ); 
As loadAllArticles$ Effect is decorated with @Effect({ dispatch: false }), so this Effect will not dispatch ShowAllSuccessAction action to Store. We can create Effects to navigate a path. We need to decorate Effect using @Effect({ dispatch: false }) and then by using Angular Router we can navigate to a path. Find a sample code.
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Effect, Actions } from '@ngrx/effects';

@Injectable()
export class AuthEffects {

  @Effect({ dispatch: false })
  loginSuccess$ = this.actions$
   .ofType(LOGIN_SUCCESS)
   .do(() => { 
         this.router.navigate(['/home'])
   });

  constructor(
       private actions$: Actions,
        private router: Router
  ) {}

}
In the above code we have created loginSuccess$ Effect. This will accept action with type LOGIN_SUCCESS and will navigate to /home path.

Angular In-Memory Web API to Test Application

Find the steps to use Angular In-Memory Web API.
1. Create a class that will implement 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. Before using In-Memory Web API we need to 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 { } 
Find the link for more information on In-Memory Web API.

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
|   | 
|   |--main.ts
|   |--index.html
|   |--styles.css
|
|--node_modules
|--package.json 
Now find the complete code.
article.effects.ts
import { Injectable } from '@angular/core';
import { Action } from '@ngrx/store';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import { of } from 'rxjs/observable/of';
import * as fromActions from '../actions/article.actions';
import { ArticleService } from '../services/article.service';

@Injectable()
export class ArticleEffects {

  constructor(
    private actions$: Actions,
    private articleService: ArticleService
  ) {}      

  @Effect() 
  loadAllArticles$: Observable<Action> = this.actions$
    .ofType(fromActions.SHOW_ALL)
    .switchMap(() => 
       this.articleService.getAllArticles()
       .map(data => new fromActions.ShowAllSuccessAction(data)) 
    );

  @Effect() 
  createArticle$: Observable<Action> = this.actions$
    .ofType<fromActions.CreateAction>(fromActions.CREATE)
    .map(action => action.payload)
    .mergeMap(article => 
       this.articleService.createArticle(article)
       .map(res => new fromActions.CreateSuccessAction(res)) 
       .catch(error => of(new fromActions.CreateFailureAction(error)))
    );
    
  @Effect() 
  searchArticleById$: Observable<Action> = this.actions$
      .ofType<fromActions.GetByIdAction>(fromActions.GET_BY_ID)
      .debounceTime(500)
      .map(action => action.payload)
      .switchMap(id => 
         this.articleService.getArticleById(id)
         .map(res => new fromActions.GetByIdSuccessAction(res)) 
      );    
} 
article.actions.ts
import { Action } from '@ngrx/store';
import { Article } from '../models/article';

export const SHOW_ALL = '[ARTICLE] Show All';
export const SHOW_ALL_SUCCESS = '[ARTICLE] Show All Success';
export const CREATE = '[ARTICLE] Create';
export const CREATE_SUCCESS = '[ARTICLE] Create Success';
export const CREATE_FAILURE = '[ARTICLE] Create Failure';
export const GET_BY_ID = '[ARTICLE] Get by Id';
export const GET_BY_ID_SUCCESS = '[ARTICLE] Get by Id Success';
export const RESET = '[ARTICLE] Reset';

export class ShowAllAction implements Action {
  readonly type = SHOW_ALL;
}
export class ShowAllSuccessAction implements Action {
  readonly type = SHOW_ALL_SUCCESS;
  constructor(public payload: Article[]) {}
}
export class CreateAction implements Action {
  readonly type = CREATE;
  constructor(public payload: Article) {}
}
export class CreateSuccessAction implements Action {
  readonly type = CREATE_SUCCESS;
  constructor(public payload: Article) {}
}
export class CreateFailureAction implements Action {
  readonly type = CREATE_FAILURE;
  constructor(public payload: any) {}
}
export class GetByIdAction implements Action {
  readonly type = GET_BY_ID;
  constructor(public payload: string) {}
}
export class GetByIdSuccessAction implements Action {
  readonly type = GET_BY_ID_SUCCESS;
  constructor(public payload: Article[]) {}
}
export class ResetAction implements Action {
  readonly type = RESET;
}

export type ALL_REDUCER_ACTIONS = ShowAllSuccessAction | CreateSuccessAction | CreateFailureAction
                 | GetByIdSuccessAction | ResetAction; 
article.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
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 } from '@ngrx/store';
import * as fromActions from '../actions/article.actions';
import { ArticleState } from './app.states';

export const initialState: ArticleState = { articles: [], message: ''};

export function reducer(state = initialState, action: fromActions.ALL_REDUCER_ACTIONS): ArticleState {
  switch(action.type) {
    case fromActions.SHOW_ALL_SUCCESS: {
      return {articles: action.payload, message: 'Success'};
    }
    case fromActions.CREATE_SUCCESS: {
      return {articles: [action.payload], message: 'Article Created.'};
    } 
    case fromActions.CREATE_FAILURE: {
      return {articles: [], message: action.payload};
    }
    case fromActions.GET_BY_ID_SUCCESS: {
      console.log(action.payload);
      return {articles: action.payload, message: 'Success'};
    }   
    case fromActions.RESET: {
      return { articles: [], message: ''};
    }  
    default: {
      return state;
    }
  }	
}

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, ActionReducer, MetaReducer } from '@ngrx/store';
import { AppState } from './app.states';
import * as fromReducer from './article.reducer';
import { environment } from '../../environments/environment';

export const reducers: ActionReducerMap<AppState> = {
  articleState: fromReducer.reducer
};

export function logger(reducer: ActionReducer<AppState>): ActionReducer<AppState> {
  return function(state: AppState, action: any): AppState {
    console.log('state', state);
    console.log('action', action);
    return reducer(state, action);
  };
}

export const metaReducers: MetaReducer<AppState>[] = !environment.production
  ? [logger]
  : []; 
article.component.ts
import { FormBuilder, Validators } from '@angular/forms'
import { Store } from '@ngrx/store';
import { Component } from '@angular/core';    
import { Observable } from 'rxjs/Observable';
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(new fromActions.ResetAction());
	}
	getArticleByIdView(){
		this.task = 'get';
		this.store.dispatch(new fromActions.ResetAction());
	}
	loadAllArticles(){
		this.task = 'all';
		this.store.dispatch(new fromActions.ShowAllAction());
	}
	createArticle(article: Article){
		this.store.dispatch(new fromActions.CreateAction(article));
	}
	searchArticleById(articleId: string){
		this.store.dispatch(new fromActions.GetByIdAction(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.errors?.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)]="articleId" (input)="searchArticleById(articleId)">  </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, metaReducers } 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, {metaReducers}),
      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@0.5.3 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 4 Example

References

@ngrx/effects
@ngrx/store
Angular QuickStart

Download Source Code

POSTED BY
ARVIND RAI
ARVIND RAI
FIND MORE TUTORILAS


©2018 concretepage.com | Privacy Policy | Contact Us