NGXS Firestore
Demo
Resources
Description
NGXS Firestore plugin helps you integrate Firestore and NGXS. It uses @angular/fire
under the hood and provides a
wrapper service with CRUD operations methods and easy integration with NGXS actions. In addition provides tracking of
active connections.
Quick start
Install the plugin:
npm install --save @ngxs-labs/firestore-plugin
yarn add @ngxs-labs/firestore-plugin
In your app.module.ts
include the plugin, note that we also include AngularFireModule
and NgxsModule
, as they are
peer dependencies for the plugin.
import { AngularFireModule } from '@angular/fire';
import { NgxsModule } from '@ngxs/store';
import { NgxsFirestoreModule } from '@ngxs-labs/firestore-plugin';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
@NgModule({
declarations: [AppComponent, ListComponent],
imports: [
AngularFireModule.initializeApp(environment.firebase),
NgxsModule
.forRoot
(),
NgxsLoggerPluginModule.forRoot(),
NgxsReduxDevtoolsPluginModule.forRoot(),
NgxsFirestoreModule.forRoot()
]
})
export class AppModule {}
Next create a service (i.e races.firestore.ts
) to execute Firestore operations. This service extends NgxsFirestore
,
a generic service that takes type <T>
of the Firestore document. We also need to provide the path
of the Firestore
collection.
import { NgxsFirestore } from '@ngxs-labs/firestore-plugin';
@Injectable({
providedIn: 'root'
})
export class RacesFirestore extends NgxsFirestore<Race> {
protected path = 'races';
}
Finally we create the @State
. The state will contain actions to execute the Firestore CRUD operations via the service
created previously. When getting data from Firestore, we have two options:
- Get data once
- Connect to a document or collection stream and receive changes as they occur on the Firestore server.
Connect and get data once
For the first scenario, NgxsFirestore
provides the methods docOnce$()
and collectionOnce$()
, which will get the
first emission and unsubscribe immediately after. NGXS handles subscribing to the Observable
and the action is done
once the first data is emmited.
export class GetAllOnce {
public static readonly type = '[Races] GetAllOnce';
}
@State<RacesStateModel>({
name: 'races',
defaults: {
races: []
}
})
export class RacesState {
constructor(private racesFS: RacesFirestore) {}
@Action(GetAllOnce)
getAllOnce({ getState, patchState }: StateContext<RacesStateModel>) {
return this.racesFS.collectionOnce$().pipe(
tap((races) => {
patchState({ races: races });
})
);
}
}
Connect to stream and receive data changes until disconnected
For the second scenario, the plugin provides the NgxsFirestoreConnect
service, which let's you connect an @Action
with a Firestore query and emit every new change as a separate Emitted
action.
The service connect
method takes as arguments the Action
that will trigger subscribing to the Firestore query. In
addition an opts
object with to
field, to pass the function that returns the Firestore query.
Once connection is setup, you can then create handlers for the specific events of the stream.
You can add handlers for:
-
Connected event with @Action(StreamConnected(RacesActions.GetAll))
.
This event will be fired once on first emission.
-
Emmited event with @Action(StreamEmitted(RacesActions.GetAll))
This event will be fired each observable emission. This action will return Emitted<Action, T>
where Action
is the
action bound, and T
is the type of the returned results from the Firestore query.
-
Disconnected event with @Action(StreamDisconnected(RacesActions.GetAll))
This event will be fired on observable disconnect.
import { NgxsFirestoreConnect } from '@ngxs-labs/firestore-plugin';
export class GetAll {
public static readonly type = '[Races] GetAll';
}
export class Get {
public static readonly type = '[Races] Get';
constructor(public payload: string) {}
}
export class RacesState implements NgxsOnInit {
constructor(private racesFS: RacesFirestore, private ngxsFirestoreConnect: NgxsFirestoreConnect) {}
ngxsOnInit() {
this.ngxsFirestoreConnect.connect(RacesActions.GetAll, {
to: () => this.racesFS.collection$()
});
this.ngxsFirestoreConnect.connect(RacesActions.Get, {
to: (action) => this.racesFS.doc$(action.payload),
trackBy: (action) => action.payload
});
}
@Action(StreamConnected(RacesActions.GetAll))
getAllConnected(ctx: StateContext<RacesStateModel>, { action }: Connected<RacesActions.GetAll>) {
}
@Action(StreamEmitted(RacesActions.GetAll))
getAllEmitted(ctx: StateContext<RacesStateModel>, { action, payload }: Emitted<RacesActions.Get, Race[]>) {
ctx.setState(patch({ races: payload }));
}
@Action(StreamDisconnected(RacesActions.GetAll))
getAllDisconnected(ctx: StateContext<RacesStateModel>, { action }: Disconnected<RacesActions.GetAll>) {
}
@Action(StreamEmitted(RacesActions.Get))
get(ctx: StateContext<RacesStateModel>, { action, payload }: Emitted<RacesActions.Get, Race>) {
ctx.setState(
patch({
races: iif(
(races) => !!races.find((race) => race.id === payload.id),
updateItem((race) => race.id === payload.id, patch(payload)),
insertItem(payload)
)
})
);
}
}
Once you connect to the Firestore stream you'll keep receiving every server update on a new Emitted
action, making it
easier to debug.
Adding write operations
When adding write operation to your state, you can use the helper methods from NgxsFirestore
. Since your data is
connnected from Firestore, every write operation you run in the state will trigger an emission and update the state.
export class Create {
public static readonly type = '[Races] Create';
constructor(public payload: Race) {}
}
export class RacesState implements NgxsOnInit {
@Action(RacesActions.Create)
create(ctx: StateContext<RacesStateModel>, { action }: Connected<RacesActions.Create>) {
return this.racesFS.create$(action.payload);
}
}
Getting data from Firestore and Disconnecting
After all your Firestore queries are bind to its respective Actions, you can start getting data by dispatching the
Action
like this:
this.store.dispatch(new RacesActions.GetAll());
this.store.dispatch(new RacesActions.Get(id));
If you need to disconnect you can dispatch
this.store.dispatch(new Disconnect(new RacesActions.GetAll()));
this.store.dispatch(new Disconnect(new RacesActions.Get(id)));
NgxsFirestore options
NgxsFirestore allows to setup an "id" field, and automatically set the Firestore's doc id in the response object. To do
this, you just need to setup idField
in your NgxsFirestore
class.
export class RacesFirestore extends NgxsFirestore<Race> {
idField = 'raceId';
}
Another option you can setup is a conversion on items that come from or go to Firestore. You can do this, setting a
firebase.firestore.FirestoreDataConverter
and configure toFirestore
and fromFirestore
. toFirestore
will be
applied before saving the object to Firestore and fromFirestore
will be applied when items are streamed from Firestore
to your app.
export class RacesFirestore extends NgxsFirestore<Race> {
protected path = 'races';
idField = 'raceId';
converter: firebase.firestore.FirestoreDataConverter<Race> = {
toFirestore: (value) => {
const db = { ...value };
delete db.testProp;
return db;
},
fromFirestore: (snapshot, options) => {
const data = snapshot.data(options);
return <Race>{ ...data, testProp: data.id + data.title };
}
};
}
Retrieving data from subcollections
When you need to pull data from a subcollection you can create a specific Firestore service for the subcollection. Let's
say the races collection, contains a classification subcollection, then you could setup the Firestore subcollection
service like this.
@Injectable({
providedIn: 'root'
})
export class ClassificationsFirestore extends NgxsFirestore<Race> {
protected get path() {
return `races/${this.raceId}/classifications`;
}
private _raceId = '';
public setRaceId(raceId) {
this._raceId = raceId;
}
protected get raceId() {
return this._raceId;
}
}