NgRx/Effects 4 Example
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.
Contents
- Technologies Used
- Installing NgRx/Effects
- EffectsModule
- @Effect() and Actions
- RxJS Operators
- Effects with
mergeMap
: Emit new Action - Effects with
switchMap
: Emit New Action - Non-dispatching Effects using
{ dispatch: false }
- Effects with
do
: Navigate to New Path - Angular In-Memory Web API to Test Application
- Complete Example
- Run Application
- References
- Download Source Code
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
@ngrx/store
, run following command.
npm install @ngrx/store --save
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 { }
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); }); }
@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))) );
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)) );
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); });
{ 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)) );
loadAllArticles$
Effect is decorated with @Effect({ dispatch: false })
, so this Effect will not dispatch ShowAllSuccessAction
action to Store.
Effects with do
: Navigate to New Path
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 ) {} }
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 }; } }
/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 | | | |--main.ts | |--index.html | |--styles.css | |--node_modules |--package.json
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)) ); }
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;
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); } }
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 } 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 );
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] : [];
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)); } }
<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>
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, 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.

References
@ngrx/effects@ngrx/store
Angular QuickStart