NGRX get data from Firestore (part 1)

Michael Warneke
4 min readDec 5, 2017

In this episode I want to discuss the different possibilities to load data from Firestore into an NGRX environment using the angularfire2 library. There are a lot of examples out there which are individually discussed and analysed, but there is no comparison with other procedures and combinations.

Not only fetching data from Firestore and place it somehow in the NGRX store, also the authentication handling can be tricky and has lots of different approaches. This blog will discuss this matter as well.

As I like the approach of Nx from Nrwl to set up a project with different libs in one mono repository the examples will be implemented in these libs lazy loaded.

First: State Changes in Effects class

The first part will look into the Firestore stateChanges() function and how to bring it together with NGRX.

// How a function in a db service could look like
getFakeDataStateChanges(): Observable<DocumentChangeAction[]>{
return this.afs.collection<FakeData>('fake-data').stateChanges();
}

This function returns an Observable of a list of DocumentChangeActions. These actions are very similar to the ones from NGRX. They come with a type and a payload. The type can be ‘added’, ‘modified’ or ‘removed’ and the payload will carry the document beside some other meta data.

To get the data into the store one way is to convert these actions into the store actions.

@Effect()
getData$ = this.dataDb.getFakeDataStateChanges().pipe(
mergeMap(actions => actions),
map(action => {
return {
type: `[FirstData] ${action.type}`,
payload: {
id: action.payload.doc.id,
...action.payload.doc.data()
}
};
})
);

In the above effect the actions received from Firestore are mapped into store actions. The store action types are basically the same as the actions coming from Firestore but with an identifier at the start ‘[FirstData]’.

export enum FirstActionTypes {
FIRST_DATA_ADDED = '[FirstData] added',
FIRST_DATA_MODIFIED = '[FirstData] modified',
FIRST_DATA_REMOVED = '[FirstData] removed'
}

I prefer to use enums as action types to keep it type-safe.

Finally the reducer will handle all the actions and updates the store.

export function firstReducer(state: State, action: FirstAction) {
switch (action.type) {
case FirstActionTypes.FIRST_DATA_ADDED:
return firstAdapter.addOne(action.payload, state);
case FirstActionTypes.FIRST_DATA_MODIFIED:
return firstAdapter.updateOne({
id: action.payload.id,
changes: action.payload
},state);
case FirstActionTypes.FIRST_DATA_REMOVED:
return firstAdapter.removeOne(action.payload.id, state);
default: {
return state;
}
}
}

In the above reducer the NGRX entity functionality is used to handle the list.

And that’s basically everything you need to have the store in sync with the database. The effect will start listening as soon as the effect class is instantiated and gets all items from the list. Once the data in the database changes the effect will call the appropriate store actions.

Changes made to the data in the app can be directed directly to the database without using the store or a store effect has to made the changes to the database without updating the store. That’s because the effect will update the store once the changes returning from the database. Once the changes are handled by the Firestore module the changes will return as mentioned above. This a pessimistic approach.

Pros and Cons

A big pro of this approach is less boilerplate in the store. There is no need for actions like GET_DATA, DATA_RECEIVED, DATA_RECEIVED_ERROR…

It just works and the store is always in sync with the database. Even offline capabilities are implemented in Firestore with one additional line of code.

AngularFirestoreModule.enablePersistence()

Down side with this pessimistic approach is that the user will see updates made to the data only after they have been changed in the database and returned. That could take some time and doesn’t reflect an instant native feel app with a local storage. For sure there is a way to implement this with an optimistic pattern and it might be in one of the coming episodes.

The effect will terminate with an error once the user signs off and the connection to protected data is cut. Therefor a function on the effects class can be used to have the effects only running when the user is authenticated.

ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>) {
return this.actions.ofType(AuthActionTypes.SET_AUTHENTICATED).pipe(
exhaustMap(() => resolvedEffects$.pipe(
takeUntil(this.actions.ofType(
AuthActionTypes.SET_NOT_AUTHENTICATED)))))
}

Unfortunately, with NGRX v4.1.1 there is bug in the store and with above function in place the actions fired by the effect will not reach the store. Meaning on reload the data will not be loaded. The bug is already solved by PR https://github.com/ngrx/platform/pull/570 and it should work with the next release.

Second: State Changes in Service

Instead of using an effect to update the store a service can be used to it.

@Injectable()
export class SecondService implements OnDestroy {
dataSub: Subscription;
constructor(private store: Store<any>, private db: DataDbService){
this.dataSub = this.db.getFakeDataStateChanges()
.subscribe(actions => {
actions.forEach(action =>
this.store.dispatch({
type: `[SecondData] ${action.type}`,
payload: {
id: action.payload.doc.id,
…action.payload.doc.data()
}
})
)
})
}
ngOnDestroy() {
this.dataSub.unsubscribe();
}

getData() {
return this.store.select(selectSecondAll);
}
}

In this case the risk that the effect will break the hole effect class is avoided. But as it uses subscribe to dispatch the actions the unsubscribe should not forgotten. The service should be in the module as the store or partial store.

The code can be found at https://github.com/MichaelWarneke/ngrx-firestore-examples and the first approach is settled in the lib ‘first’.

In future posts I will look into other ways of handling the same constellation and I appreciate comments and remarks to have a discussion to find betters ways of best practice.

--

--