A Beginner's Guide To Using ngrx In An Ionic 2 App - Part 2

In Part 1 of this tutorial we used @ngrx/store to manage our application state in memory. In order to persist this state, we are going to use @ngrx/effects.

We are also going to use PouchDB to save the data to a local database. I won't go into the details of PouchDB in this tutorial, since it's all explained in my tutorial for PouchDB + Ionic 2. So if you're new to PouchDB, you might want to read that first before you continue with this tutorial.

This tutorial is part of a multi-part series:
Part 1 - Setting up Store, Actions and Reducers
Part 2 - Persisting to a PouchDB database (this post)

The source code can be found on GitHub.


What is @ngrx/effects?

Remember how I said that the reducer is a pure function and doesn't have side-effects? Well, side-effects can be things like saving data to a database or doing an HTTP call to a remote API and are obviously part of the mechanics of almost every application.

@ngrx/effects provides us with a way to implement these side-effects while still keeping our reducer functions pure and easily testable.

Like in a reducer, side-effects also respond to actions that are dispatched to the store. When a side-effect has done its work, another action is dispatched that will then be handled by the reducer to update the application state.

It makes more sense to explain this when we're actually implementing this, so let's get started!

Install libraries

We need to install @ngrx/effects.

npm install @ngrx/effects --save  

We will be using PouchDB to save to a local database.

npm install pouchdb --save  

We'll use require to import the PouchDB library.

typings install dt~require --global --save  

Create database service

Before we have a look at the side-effects, let's create a service that will encapsulate the PouchDB calls. This service is responsible for storing and retrieving the data from the database.

As I mentioned before, have a look at my PouchDB + Ionic2 tutorial for a better understanding of this service.

// location: app/services/birthday.service.ts

import {Injectable} from '@angular/core';  
import {Platform} from 'ionic-angular';  
import {Observable} from 'rxjs/rx';

import {Birthday} from '../models/birthday';

let PouchDB = require('pouchdb');

@Injectable()
export class BirthdayService {  
    private db;

    constructor(private platform: Platform) { }

    initDB() : Promise<any> {
        return this.platform.ready()
                   .then(() => {
                        this.db = new PouchDB('birthday', { adapter: 'websql' });
                    });
    }

    add(birthday: Birthday) : Promise<any> {
        return this.db.post(birthday);
    }

    update(birthday: Birthday) : Promise<any> {
        return this.db.put(birthday);
    }

    delete(birthday: Birthday) : Promise<any> {
        return this.db.remove(birthday);
    }

    getAll() : Observable<any> {
        return Observable.fromPromise(
            this.initDB()
                .then(() => {
                    return this.db.allDocs({ include_docs: true });
                })
                .then(docs => {

                    // Each row has a .doc object and we just want to send an 
                    // array of birthday objects back to the calling code,
                    // so let's map the array to contain just the .doc objects.

                    return docs.rows.map(row => {
                        // Convert string to date, doesn't happen automatically.
                        row.doc.date = new Date(row.doc.date);
                        return row.doc;
                    });
                }));
    }

    getChanges(): Observable<any> {
        return Observable.create(observer => {

                // Listen for changes on the database.
                this.db.changes({ live: true, since: 'now', include_docs: true })
                    .on('change', change => {
                        // Convert string to date, doesn't happen automatically.
                        change.doc.date = new Date(change.doc.date);
                        observer.next(change.doc);
                    });
        });
    }
}

Update the model

In the previous part, we had an id on the Birthday model that we were generating ourself in the reducer. Now we are going to use the _id that is generated by PouchDB, so let's update the model to reflect that.

// location: app/model/birthday.ts

export interface Birthday {  
    _id: string;
    name: string;
    date: Date;
}

Define new actions

We also have to add a couple more actions in our actions class. These actions will be dispatched when the side-effects have completed their work.

// location: app/actions/birthday.actions.ts

static LOAD_BIRTHDAYS_SUCCESS = 'LOAD_BIRTHDAYS_SUCCESS';  
loadBirthdaysSuccess(birthdays: Birthday[]): Action {  
    return {
        type: BirthdayActions.LOAD_BIRTHDAYS_SUCCESS,
        payload: birthdays
    }
}

static ADD_UPDATE_BIRTHDAY_SUCCESS = 'ADD_UPDATE_BIRTHDAY_SUCCESS';  
addUpdateBirthdaySuccess(birthday: Birthday): Action {  
    return {
        type: BirthdayActions.ADD_UPDATE_BIRTHDAY_SUCCESS,
        payload: birthday
    }
}

static DELETE_BIRTHDAY_SUCCESS = 'DELETE_BIRTHDAY_SUCCESS';  
deleteBirthdaySuccess(id: string): Action {  
    return {
        type: BirthdayActions.DELETE_BIRTHDAY_SUCCESS,
        payload: id
    }
}

Implement side-effects

Let's have a look at how we are going to implement the add/update/delete side-effects for our actions.

We'll create a new class and call it BirthdayEffects and we need to inject a couple of providers in its constructor: StateUpdates, BirthdayService, and BirthdayActions.

The StateUpdates observable will let us know when a new action was dispatched to the store.

For every side-effect, we create a new observable that responds to a specific action (using whenAction) and calls the corresponding function on the database service.

The @Effect decorator should be placed on every observable side-effect so @ngrx/effects can connect them to the store.

// location: app/effects/birthday.effects.ts

import {Injectable} from '@angular/core';  
import {Effect, StateUpdates, toPayload} from '@ngrx/effects';  
import {Store} from '@ngrx/store';  
import {Observable} from 'rxjs/rx';

import {AppState} from '../services/appstate';  
import {BirthdayService} from '../services/birthday.service';  
import {Birthday} from '../models/birthday';  
import {BirthdayActions} from '../actions/birthday.actions';

@Injectable()
export class BirthdayEffects {

    constructor(
        private updates$: StateUpdates<AppState>,
        private db: BirthdayService,
        private birthdayActions: BirthdayActions
    ) { }

    @Effect() addBirthday$ = this.updates$
        .whenAction(BirthdayActions.ADD_BIRTHDAY)
        .map<Birthday>(toPayload)
        .mergeMap(birthday => this.db.add(birthday));

    @Effect() updateBirthday$ = this.updates$
        .whenAction(BirthdayActions.UPDATE_BIRTHDAY)
        .map<Birthday>(toPayload)
        .mergeMap(birthday => this.db.update(birthday));

    @Effect() deleteBirthday$ = this.updates$
        .whenAction(BirthdayActions.DELETE_BIRTHDAY)
        .map<Birthday>(toPayload)
        .mergeMap(birthday => this.db.delete(birthday));
}

As you might notice, we aren't sending another action when the database task has completed. That's because we are going to use the getChanges function on BirthdayService to get notified when a change occurred in the database.

In the code below you can see how we are first calling getAll and when that's done, we are mapping that to the action LOAD_SUCCESS.

After that we are calling getChanges and depending on the change, we either return ADD_UPDATE_BIRTHDAY_SUCCESS or DELETE_BIRTHDAY_SUCCESS.

These calls will be executed on initialization of the application.

// location: app/effects/birthday.effects.ts

allBirthdays$ = this.db.getAll()  
    .map(birthdays => this.birthdayActions.loadBirthdaysSuccess(birthdays));

changedBirthdays$ = this.db.getChanges()  
    .map(change => {
        if (change._deleted) {
            return this.birthdayActions.deleteBirthdaySuccess(change._id);
        } 
            else {
                return this.birthdayActions.addUpdateBirthdaySuccess(change);
            }
        });

@Effect() getBirthdays$ = Observable.concat(this.allBirthdays$, this.changedBirthdays$);

Update the reducer

Now we need to update the BirthdaysReducer function to handle the actions that are dispatched from BirthdayEffects.

// location: app/reducers/birthdays.reducer.ts

import {ActionReducer, Action} from '@ngrx/store';  
import {BirthdayActions} from '../actions/birthday.actions';

import {Birthday} from '../models/birthday';

export const BirthdaysReducer: ActionReducer<Birthday[]> = (state: Birthday[] = [], action: Action) => {  
    switch(action.type) {
        case BirthdayActions.LOAD_BIRTHDAYS_SUCCESS:
            return action.payload;
        case BirthdayActions.ADD_UPDATE_BIRTHDAY_SUCCESS:
            var exists = state.find(birthday => birthday._id === action.payload._id);

            if (exists) {
                // UPDATE
                return state.map(birthday => {
                    return birthday._id === action.payload._id ? Object.assign({}, birthday, action.payload) : birthday;
                });
            } 
            else {
                // ADD
                return [...state, Object.assign({}, action.payload)];
            }
        case BirthdayActions.DELETE_BIRTHDAY_SUCCESS:
            return state.filter(birthday => birthday._id !== action.payload);
        default:
            return state;
    };
}

Update ionicBootstrap

The final thing to do now is add BirthdayService and BirthdayEffects to the bootstrapper in app.ts.

Add these imports:

// location: app/app.ts

import {runEffects} from '@ngrx/effects';

import {BirthdayEffects} from './effects/birthday.effects';  
import {BirthdayService} from './services/birthday.service';  

Update ionicBootstrap to pass in the 2 new providers.

ionicBootstrap(  
  MyApp, 
  [
    provideStore({ birthdays: BirthdaysReducer }), 
    runEffects(BirthdayEffects),
    BirthdayService,
    BirthdayActions
  ]);

We're done!

You can now test the app in the browser.

$ ionic serve

And on your iOS and Android devices.

$ ionic run ios
$ ionic run android

The source code can be found on GitHub.


What's Next?

That's all for this tutorial series for now. I might add related tutorials later on, but if you want to learn more, the best thing you can do is have a look at the official ngrx example app source code and have a look at the other ngrx libraries.

Also check out the References section in Part 1 for more learning material.

Follow me on Twitter @ashteya and sign up for my weekly emails to get new tutorials.

If you found this article useful, could you hit the share buttons so that others can benefit from it, too? Thanks!

comments powered by Disqus