Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Ngrx Effects With Firestore and AngularFire V5

Episode 61 written by Jeff Delaney
full courses and content on fireship.io



In this lesson, I will show you how to integrate AngularFire v5 with ngrx to handle documents and collections in the Firestore database. We will create an app that keeps data in-sync between the ngrx store frontend and the Firestore database backend.

This tutorial uses code directly from my previous Ngrx Entity tutorial. It updates the simple pizza app to use Firestore for backend data persistence. I highly recommend that you start there first.

Full Source Code


Source code link available to pro members. Learn more



Get the full ngrx firestore source code on github.


ngrx with firestore as persistent backend database

Adding Effects to the Modules

In the previous lesson, we created an ngrx feature module. We need to (1) add firebase and (2) add ngrx effects to the existing modules.

First, let’s update the app module with firebase and an empty effects array.

// app.module
// ...omitted see github source
import { AngularFireModule } from 'angularfire2';
import { environment } from '../environments/environment';

import { AngularFirestoreModule } from 'angularfire2/firestore';

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

@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
StoreModule.forRoot(reducers),
StoreDevtoolsModule.instrument({
maxAge: 25
}),
AngularFireModule.initializeApp(environment.firebaseConfig),
AngularFirestoreModule,
EffectsModule.forRoot([]), // <--- effects here
PizzaModule,
],
bootstrap: [AppComponent]
})
export class AppModule { }

Then update the pizza module with the actual effects that will be created later.

// pizza.module
// ...omitted see github source
import { PizzaEffects } from './pizza.effects'

@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('pizza', pizzaReducer),
EffectsModule.forFeature([PizzaEffects])
],
exports: [PizzaOrderComponent],
declarations: [PizzaOrderComponent]
})
export class PizzaModule { }

Actions

We need to add a few new actions to deal with the asynchronous reading and writing of data in Firestore. Here’s a breakdown of the new actions, all the others will stay exactly the same.

  • QUERY - Makes the initial request for a firestore collection.
  • ADD_ALL - Takes the returned array of documents and adds them to ngrx store.
  • SUCCESS - Generic action that represents successful write operation in Firestore.
import { Action } from '@ngrx/store';
import { Pizza } from './pizza.reducer';

export const CREATE = '[Pizzas] Create'
export const UPDATE = '[Pizzas] Update'
export const DELETE = '[Pizzas] Delete'

export const QUERY = '[Pizzas] Query'
export const ADD_ALL = '[Pizzas] Add All'
export const SUCCESS = '[Pizzas] Successful firestore write'

export class Query implements Action {
readonly type = QUERY;
constructor() { }
}

export class AddAll implements Action {
readonly type = ADD_ALL;
constructor(public pizzas: Pizza[]) { }
}

export class Success implements Action {
readonly type = SUCCESS;
constructor() { }
}


export class Create implements Action {
readonly type = CREATE;
constructor(public pizza: Pizza) { }
}

export class Update implements Action {
readonly type = UPDATE;
constructor(
public id: string,
public changes: Partial<Pizza>,
) { }
}

export class Delete implements Action {
readonly type = DELETE;
constructor(public id: string) { }
}

export type PizzaActions
= Create
| Update
| Delete
| Query
| AddAll;

Reducer

The reducer is going to get much simpler. All of our CRUD operations are now asynchronous and handled via effects. The only thing we need our reducer to do is update the collection with new objects when the data changes. Firestore always returns the full array of documents when a change occurs, so using addAll() seems to be the best approach when working with Firstore collections and @ngrx/entity.

I also removed the default data because we will now be retrieving all persistent data from Firestore.

import * as actions from './pizza.actions';
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { createFeatureSelector } from '@ngrx/store';

// Main data interface

export interface Pizza {
id: string;
size: string;
}

// Entity adapter
export const pizzaAdapter = createEntityAdapter<Pizza>();
export interface State extends EntityState<Pizza> { }

export const initialState: State = pizzaAdapter.getInitialState();

// Reducer

export function pizzaReducer(
state: State = initialState,
action: actions.PizzaActions) {

switch (action.type) {

case actions.ADD_ALL:
return pizzaAdapter.addAll(action.pizzas, state);

default:
return state;
}

}

// Create the default selectors

export const getPizzaState = createFeatureSelector<State>('pizza');

export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = pizzaAdapter.getSelectors(getPizzaState);

Ngrx Effects with Firestore

All of the important work happens in the effects, starting with the first $query effect that retrieves a Firestore collection.

Our interaction with Firebase is asynchronous, so we are going to listen to certain initial actions with the @effect decorator, then map them to a different action after performing an async operation. The effects code is very RxJS heavy and may look intimidating if you’re new to this stuff, but what it does is actually quite simple:

  1. It listens to an action type that triggers an async event, i.e CREATE.
  2. Makes the request with AngularFire, which returns a Promise (or Observable).
  3. Maps the result to an action, i.e SUCCESS or ERROR.

Just remember, all of the effects below follow this same three-step process.


@Injectable()
export class PizzaEffects {
// Listen for the 'QUERY' action

@Effect() query$: Observable<Action> = this.actions$.ofType(actions.QUERY)



import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/observable/fromPromise';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Action } from '@ngrx/store';
import { Actions, Effect } from '@ngrx/effects';

import * as actions from './pizza.actions';
import * as fromPizza from './pizza.reducer';
import { AngularFirestore, AngularFirestoreCollection } from 'angularfire2/firestore';

@Injectable()
export class PizzaEffects {

// Listen for the 'QUERY' action, must be the first effect you trigger

@Effect() query$: Observable<Action> = this.actions$.ofType(actions.QUERY)
.switchMap(action => {
const ref = this.afs.collection<fromPizza.Pizza>('pizzas')
return ref.snapshotChanges().map(arr => {
return arr.map( doc => {
const data = doc.payload.doc.data()
return { id: doc.payload.doc.id, ...data } as fromPizza.Pizza
})
})
})
.map(arr => {
console.log(arr)
return new actions.AddAll(arr)
})

// Listen for the 'CREATE' action

@Effect() create$: Observable<Action> = this.actions$.ofType(actions.CREATE)
.map((action: actions.Create) => action.pizza )
.switchMap(pizza => {
const ref = this.afs.doc<fromPizza.Pizza>(`pizzas/${pizza.id}`)
return Observable.fromPromise( ref.set(pizza) )
})
.map(() => {
return new actions.Success()
})

// Listen for the 'UPDATE' action

@Effect() update$: Observable<Action> = this.actions$.ofType(actions.UPDATE)
.map((action: actions.Update) => action)
.switchMap(data => {
const ref = this.afs.doc<fromPizza.Pizza>(`pizzas/${data.id}`)
return Observable.fromPromise( ref.update(data.changes) )
})
.map(() => {
return new actions.Success()
})

// Listen for the 'DELETE' action

@Effect() delete$: Observable<Action> = this.actions$.ofType(actions.DELETE)
.map((action: actions.Delete) => action.id )
.switchMap(id => {
const ref = this.afs.doc<fromPizza.Pizza>(`pizzas/${id}`)
return Observable.fromPromise( ref.delete() )
})
.map(() => {
return new actions.Success()
})

constructor(private actions$: Actions, private afs: AngularFirestore) { }
}


Component

Lastly, we need to update the component. The only difference is that we need to run the QUERY action to load the initial data. Once this happens, our ngrx store and Firebase will be synced up with a realtime connection.

Only one line has changed from the previous lesson this.store.dispatch( new actions.Query() ), which queries the initial data when the component is initialized.

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import * as actions from '../pizza.actions';
import * as fromPizza from '../pizza.reducer';

@Component({
selector: 'pizza-order',
templateUrl: './pizza-order.component.html',
styleUrls: ['./pizza-order.component.sass']
})
export class PizzaOrderComponent implements OnInit {

pizzas: Observable<any>;

constructor(private store: Store<fromPizza.State>) { }

ngOnInit() {
this.pizzas = this.store.select(fromPizza.selectAll)

this.store.dispatch( new actions.Query() ) /// <--- new part here
}

createPizza() {
const pizza: fromPizza.Pizza = {
id: new Date().getUTCMilliseconds().toString(),
size: 'small'
}

this.store.dispatch( new actions.Create(pizza) )
}

updatePizza(id, size) {
this.store.dispatch( new actions.Update(id, { size: size }) )
}

deletePizza(id) {
this.store.dispatch( new actions.Delete(id) )
}

}

The End

Having a persistent backend is required for almost all apps and Firestore makes it easy. Let me know what you think in the comments or on Slack.