
Introduction to @ngrx/effects
This article builds on Part 1: Introduction to @ngrx/store and adds the use of @ngrx/effects to handle side-effects within our application.
We'll be building out functionality to deal with asynchronous requests, loading indicators, and more. For example, whilst loading, we can change the colour of our drop shadow:

What's a side effect?
Prior to adding @ngrx/effects, we firstly have to consider what a side effect is.
A lot of the time this can be boiled down to - am I dealing with the network or another asynchronous task?
By passing side-effects to our @Effect
middleware and isolating this from our component(s), we're able to keep our components lighter and more responsive to change.
We're able to do this as Effects
listens to every Action
dispatched within our Store
. These actions can then be filtered and we can then use this to drive state changes.
With that in mind, let's upgrade our @ngrx/store
Shopping List to use @ngrx/effects
, starting with our API.
Http and json-server
In order to persist our shopping items, we'll need a database and API of some form. As we're in prototype land, using something like json-server
is a great choice here.
json-server
is a file-based database that exposes a REST API based on the JSON schema. In the root of our project (/
), create a db.json
with the following content:
{
"shopping": []
}
Then install json-server
globally on your machine using npm
or yarn
:
$ npm i json-server -g
Finally, start your database and API by opening another terminal window and typing:
$ json-server db.json
\{^_^}/ hi!
Loading db.json
Done
Resources
http://localhost:3000/shopping
Home
http://localhost:3000
Type s + enter at any time to create a snapshot of the database
If we make a request to http://localhost:3000/shopping
(using curl
, Postman, or something similar), we get back an empty array:
$ curl http://localhost:3000/shopping
[]
If I go ahead and add an item and do the same, we get:
{
"shopping": [
{
"id": 1,
"name": "Diet Coke"
}
]
}
$ json-server db.json
$ curl http://localhost:3000/shopping
[
{
"id": 1,
"name": "Diet Coke"
}
]
When we change the database manually from the filesystem we have to restart it, but adding items via the API doesn't require a restart.
We can also do things like:
$ curl https://localhost:3000/shopping/1
{
"id": 1,
"name": "Diet Coke"
}
Now that we're confident in our prototype API, we can go ahead and create a ShoppingService
with the Angular CLI to interface with this.
ShoppingService
Inside of your terminal, run the following:
$ ng generate service Shopping
We can then use the HttpClient
to interface with our API. I've added three core functions, get
, post
and delete
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ShoppingItem } from './store/models/shopping-item.model';
import { delay } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class ShoppingService {
private SHOPPING_URL = "http://localhost:3000/shopping"
constructor(private http: HttpClient) { }
getShoppingItems() {
return this.http.get<Array<ShoppingItem>>(this.SHOPPING_URL)
.pipe(
delay(500)
)
}
addShoppingItem(shoppingItem: ShoppingItem) {
return this.http.post(this.SHOPPING_URL, shoppingItem)
.pipe(
delay(500)
)
}
deleteShoppingItem(id: string) {
return this.http.delete(`${this.SHOPPING_URL}/${id}`)
.pipe(
delay(500)
)
}
}
Why is there a delay
here?
I want to show an example of a loading
state within our UI, although it isn't necessary, it's a fun detail. Without the delay
, this'd be instantaneous and harder to see.
Finally, we'll need to import
the HttpClientModule
inside of our app.module.ts
:
import { HttpClientModule } from '@angular/common/http';
@NgModule({
// Omitted
imports: [
HttpClientModule
]
})
Async-friendly Actions
Let's change up our ShoppingActionTypes
to be more async-friendly:
export enum ShoppingActionTypes {
LOAD_SHOPPING = '[SHOPPING] Load Shopping',
LOAD_SHOPPING_SUCCESS = '[SHOPPING] Load Shopping Success',
LOAD_SHOPPING_FAILURE = '[SHOPPING] Load Shopping Failure',
ADD_ITEM = '[SHOPPING] Add Item',
ADD_ITEM_SUCCESS = '[SHOPPING] Add Item Success',
ADD_ITEM_FAILURE = '[SHOPPING] Add Item Failure',
DELETE_ITEM = '[SHOPPING] Delete Item',
DELETE_ITEM_SUCCESS = '[SHOPPING] Delete Item Success',
DELETE_ITEM_FAILURE = '[SHOPPING] Delete Item Failure'
}
As an example, this now gives us the potential to call the LOAD_SHOPPING
action and based on the server response, it should either dispatch
a LOAD_SHOPPING_SUCCESS
or LOAD_SHOPPING_FAILURE
action with a supporting payload.
Let's export these actions so that they can be dispatched:
export class LoadShoppingAction implements Action {
readonly type = ShoppingActionTypes.LOAD_SHOPPING
}
export class LoadShoppingSuccessAction implements Action {
readonly type = ShoppingActionTypes.LOAD_SHOPPING_SUCCESS
constructor(public payload: Array<ShoppingItem>) {}
}
export class LoadShoppingFailureAction implements Action {
readonly type = ShoppingActionTypes.LOAD_SHOPPING_FAILURE
constructor(public payload: Error) {}
}
export class AddItemAction implements Action {
readonly type = ShoppingActionTypes.ADD_ITEM
constructor(public payload: ShoppingItem) { }
}
export class AddItemSuccessAction implements Action {
readonly type = ShoppingActionTypes.ADD_ITEM_SUCCESS
constructor(public payload: ShoppingItem) { }
}
export class AddItemFailureAction implements Action {
readonly type = ShoppingActionTypes.ADD_ITEM_FAILURE
constructor(public payload: Error) { }
}
export class DeleteItemAction implements Action {
readonly type = ShoppingActionTypes.DELETE_ITEM
constructor(public payload: string) { }
}
export class DeleteItemSuccessAction implements Action {
readonly type = ShoppingActionTypes.DELETE_ITEM_SUCCESS
constructor(public payload: string) { }
}
export class DeleteItemFailureAction implements Action {
readonly type = ShoppingActionTypes.DELETE_ITEM_FAILURE
constructor(public payload: string) { }
}
export type ShoppingAction = AddItemAction |
AddItemSuccessAction |
AddItemFailureAction |
DeleteItemAction |
DeleteItemSuccessAction |
DeleteItemFailureAction |
LoadShoppingAction |
LoadShoppingFailureAction |
LoadShoppingSuccessAction
Updating our AppState
As we're dealing with asynchronous data, we have the option to show the fact that we're loading
and a potential error
inside of our UI.
When the user interacts with our list, we'll be updating the box-shadow
with a new color cause we're fancy like that!
import { ShoppingState } from './../reducers/shopping.reducer';
export interface AppState {
readonly shopping: ShoppingState
}
We can export the ShoppingState
from our shopping.reducer.ts
, as well as update any prior definitions:
export interface ShoppingState {
list: ShoppingItem[],
loading: boolean,
error: Error
}
const initialState: ShoppingState = {
list: [],
loading: false,
error: undefined
};
export function ShoppingReducer(state: ShoppingState = initialState, action: ShoppingAction) {
// Omitted
}
Reducer updates
Next up, we'll have to update our ShoppingReducer
to handle the ability to switch
on our new actions. Let's see how this looks:
export function ShoppingReducer(state: ShoppingState = initialState, action: ShoppingAction) {
switch (action.type) {
case ShoppingActionTypes.LOAD_SHOPPING:
return {
...state,
loading: true
}
case ShoppingActionTypes.LOAD_SHOPPING_SUCCESS:
return {
...state,
list: action.payload,
loading: false
}
case ShoppingActionTypes.LOAD_SHOPPING_FAILURE:
return {
...state,
error: action.payload,
loading: false
}
case ShoppingActionTypes.ADD_ITEM:
return {
...state,
loading: true
}
case ShoppingActionTypes.ADD_ITEM_SUCCESS:
return {
...state,
list: [...state.list, action.payload],
loading: false
};
case ShoppingActionTypes.ADD_ITEM_FAILURE:
return {
...state,
error: action.payload,
loading: false
};
case ShoppingActionTypes.DELETE_ITEM:
return {
...state,
loading: true
};
case ShoppingActionTypes.DELETE_ITEM_SUCCESS:
return {
...state,
list: state.list.filter(item => item.id !== action.payload),
loading: false
}
case ShoppingActionTypes.DELETE_ITEM_FAILURE:
return {
...state,
error: action.payload,
loading: false
};
default:
return state;
}
}
We can make our reducer much lighter by using @ngrx/entity
, and this is something we'll be investigating in a future article.
Meanwhile, our ShoppingReducer
is now effectively setting loading
to true
each time we make an asynchronous call. Once that action either succeeds or fails, we're resetting loading
and using the action.payload
to drive our UI.
Using @Effect()
We're finally at the part that looks at how to use @ngrx/effects
!
Install @ngrx effects by running the following in your terminal:
$ npm install @ngrx/effects
As always, you can use the Angular CLI to set this up automatically:
$ ng add @ngrx/effects
Next, create a new file at store/effects/shopping.effects.ts
and let's make an @Effect()
for loadShopping
:
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { LoadShoppingAction, ShoppingActionTypes, LoadShoppingSuccessAction, LoadShoppingFailureAction } from '../actions/shopping.actions'
import { of } from 'rxjs';
import { ShoppingService } from 'src/app/shopping.service';
@Injectable()
export class ShoppingEffects {
@Effect() loadShopping$ = this.actions$
.pipe(
ofType<LoadShoppingAction>(ShoppingActionTypes.LOAD_SHOPPING),
mergeMap(
() => this.shoppingService.getShoppingItems()
.pipe(
map(data => {
return new LoadShoppingSuccessAction(data)
}),
catchError(error => of(new LoadShoppingFailureAction(error)))
)
),
)
constructor(
private actions$: Actions,
private shoppingService: ShoppingService
) { }
}
- The
@Effect()
decorator allows @ngrx to handle theloadShopping$
Observable and this means we don't have to handle the registration/subscriptions within theStore
. - Next, we're listening to
actions
and usingofType
to gain access to theAction
that matchesLoadShoppingAction
. - We can then use
mergeMap
tomerge
these streams and callgetShoppingItems()
which returns us an Observable with our shopping list items. - With those shopping list items in hand, we can
map
over the response and either dispatch anew LoadShoppingSuccessAction(data)
if this was successful, otherwise usecatchError
to dispatch an Observableof(new LoadShoppingFailureAction(error))
.
This pattern is similar for the rest of our effects:
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { LoadShoppingAction, ShoppingActionTypes, LoadShoppingSuccessAction, LoadShoppingFailureAction, AddItemAction, AddItemSuccessAction, AddItemFailureAction, DeleteItemAction, DeleteItemSuccessAction, DeleteItemFailureAction } from '../actions/shopping.actions'
import { of } from 'rxjs';
import { ShoppingService } from 'src/app/shopping.service';
@Injectable()
export class ShoppingEffects {
@Effect() loadShopping$ = this.actions$
.pipe(
ofType<LoadShoppingAction>(ShoppingActionTypes.LOAD_SHOPPING),
mergeMap(
() => this.shoppingService.getShoppingItems()
.pipe(
map(data => {
return new LoadShoppingSuccessAction(data)
}),
catchError(error => of(new LoadShoppingFailureAction(error)))
)
),
)
@Effect() addShoppingItem$ = this.actions$
.pipe(
ofType<AddItemAction>(ShoppingActionTypes.ADD_ITEM),
mergeMap(
(data) => this.shoppingService.addShoppingItem(data.payload)
.pipe(
map(() => new AddItemSuccessAction(data.payload)),
catchError(error => of(new AddItemFailureAction(error)))
)
)
)
@Effect() deleteShoppingItem$ = this.actions$
.pipe(
ofType<DeleteItemAction>(ShoppingActionTypes.DELETE_ITEM),
mergeMap(
(data) => this.shoppingService.deleteShoppingItem(data.payload)
.pipe(
map(() => new DeleteItemSuccessAction(data.payload)),
catchError(error => of(new DeleteItemFailureAction(error)))
)
)
)
constructor(
private actions$: Actions,
private shoppingService: ShoppingService
) { }
}
We'll then need to add our Effects via an EffectsModule
. As we're not using a feature module at this stage, we'll be using EffectsModule.forRoot()
in comparison to EffectsModule.forFeature()
:
// app.module.ts
imports: [
EffectsModule.forRoot([ShoppingEffects]),
]
Hooking up our UI
Let's update our template to take advantage of our fancy Effects:
<div id="wrapper">
<div [class.loading]="(loading$ | async)" id="shopping-list" *ngIf="!(error$ | async); else error">
<div id="list">
<h2>
Shopping List
</h2>
<ul *ngIf="(shoppingItems | async); else noItems">
<li *ngFor="let shopping of shoppingItems | async" (click)="deleteItem(shopping.id)">
<span>{{ shopping.name }}</span>
</li>
</ul>
<ng-template #noItems>
<ul>
<li style="max-width:250px;margin:0 auto;">No items found. Are you sure there's <em>nothing</em> you want?</li>
</ul>
</ng-template>
</div>
<form (ngSubmit)="addItem()">
<input type="text" [(ngModel)]="newShoppingItem.name" placeholder="Item" name="itemName"/>
<button type="submit" >+</button>
</form>
</div>
</div>
<ng-template #error>
<h2>{{(error$ | async)?.message}}</h2>
</ng-template>
Nothing too crazy going on here. We're subscribing to selections of our state, and when we're loading we show the loading
class:
.loading {
box-shadow: 20px 20px 0px pink !important;
}
Finally, this is what our app.component.ts
looks like:
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { v4 as uuid } from 'uuid';
import { AppState } from './store/models/app-state.model';
import { ShoppingItem } from './store/models/shopping-item.model';
import { AddItemAction, DeleteItemAction, LoadShoppingAction } from './store/actions/shopping.actions';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
shoppingItems: Observable<Array<ShoppingItem>>;
loading$: Observable<Boolean>;
error$: Observable<Error>
newShoppingItem: ShoppingItem = { id: '', name: '' }
constructor(private store: Store<AppState>) { }
ngOnInit() {
this.shoppingItems = this.store.select(store => store.shopping.list);
this.loading$ = this.store.select(store => store.shopping.loading);
this.error$ = this.store.select(store => store.shopping.error);
this.store.dispatch(new LoadShoppingAction());
}
addItem() {
this.newShoppingItem.id = uuid();
this.store.dispatch(new AddItemAction(this.newShoppingItem));
this.newShoppingItem = { id: '', name: '' };
}
deleteItem(id: string) {
this.store.dispatch(new DeleteItemAction(id));
}
}
When we click around our application and perform actions such as LoadShoppingAction
, AddShoppingAction
and DeleteShoppingAction
, we'll see our Effects
kick in within the Redux DevTools:

Once again, there's many optimisations we can make here (selectors being one of them), and that's something I'll be continuing to cover in future @ngrx articles.
Summary
We did it! We updated the application to use @ngrx/effects
.
The code for this article is under the effects
branch on the repo at: https://github.com/developer-school/introduction-to-ngrx-store.
developer.school Newsletter
Join the newsletter to receive the latest updates in your inbox.