{"community_link":"https://community.indepth.dev/t/firebase-ngxs-the-perfect-couple/855"}

Firebase + NGXS, the perfect couple

Easily integrate Firebase with NGXS to keep all your data synced in the store

Firebase + NGXS, the perfect couple

A few years ago Firebase was a game-changer for web-app development. Designed for high performance and ease-of-use, developer’s productivity was boosted and millions of apps were built since then.

Later on, NGXS brought to the Angular ecosystem a new reactive state management library. With simplicity and code ergonomics as its core focus, it helped thousands of developers to make the dreaded state management problem go away.

But how can we bring them together to take things to the next level?

NGXS recently introduced a plugin called @ngxs-labs/firestore-plugin which provides a simple yet flexible API to connect an @Action to a Firestore query, and its query results to the store, allowing you to easily keep all your app data in a single place, making things much easier to select and combine data from different origins (Firestore, REST APIs, etc) and displaying it in your components.

In this tutorial we'll go over how the plugin works and a few examples of how to use it.


How does it work?

Let's say you have a @State, and you want to fetch items from a Firestore collection into the state. Using firestore-plugin all you need to do is:

  • Connect an @Action to a Firestore query
  • Listen for query results in the StreamEmmitted Action handler and update the state accordingly.
Ngxs Firestore Plugin

That's it. That's all you need. Now let's take a closer look how to do this and how it works under the hood.

Connecting an Action to a Firestore query

The plugin provides the NgxsFirestoreConnect service, which takes care of connecting an Action with an observable Firestore query.

this.ngxsFirestoreConnect.connect(MyAction, {
  to: () => this.firestore.collection$()
})

Here, connect method takes the Action and { to: } param where we pass a function that returns the query results as an observable.

The plugin also includes the NgxsFirestore<T> abstract class, which we can extend from to quickly setup a service to connect with Firestore and perform common operations such as get items, create, update and delete.

We can write our query using AngularFirebase's service or use the NgxsFirestore<T> utility abstract class.

export abstract class NgxsFirestore<T> { 
  protected abstract path: string; 
  
  doc$(id: string): Observable<T>
  collection$(query: queryFn): Observable<T[]>
  create$(value: T): Observable<void>
  upsert$(value: T): Observable<void>
  //...
}

We can easily extend from it, and we just need to define the path of the collection we'll query to. The service provides a simple API to perform common CRUD operations such as (doc$, collection$, update$, create$, upsert$, delete$)

@Injectable({
  providedIn: 'root'
})
export class MyFirestoreService extends NgxsFirestore<Race> {
  protected path = {firestore collection path};
}

It's important to notice that the query will not get executed until you dispatch MyAction. Once the query is executed, NgxsFirestoreConnect creates a subscription that will be kept alive until you explicitly Disconnect from it. This means that all changes that occur on the Firestore DB will be emitted.

The place to set up this connection is the ngxsOnInit lifecycle hook of our @State.

Here's the complete example:

@State({
//...
})
@Injectable()
export class MyState implements NgxsOnInit {
  constructor(
    private ngxsFirestoreConnect: NgxsFirestoreConnect,
    private firestore: AngularFirestore,
  ){ }

  ngxsOnInit(){
    this.ngxsFirestoreConnect.connect(MyAction, {
      to: () => this.firestore.collection$()
    })
  }
}

So far, we established a connection between an Action and a Firestore query.

Next, we'll see how to grab those results as they are emitted, and update the state.

Updating store when query results are emitted

Once connection is setup, and connected Action executed, the plugin will dispatch a new action every time the query emits a result. In order to listen to each emission we use the StreamEmitted action helper.

@Action(StreamEmitted(MyAction))

StreamEmitted is a function that takes the connected Action as param, and creates the hook for each value the query emits.

To complete the @Action handler definition, we declare the parameters, which are the StateContext and the StreamEmitted payload. The Emitted type contains the connected Action and the query result.

To read more about actions on NGXS check the docs
type Emitted<A, T> = {
  action: A,
  payload: T
}

Since Emitted is generic, we can type it Emitted<MyAction, Item[]> to add type safety in the action.

The complete code would look like this:

@Action(StreamEmitted(MyAction))
myActionEmmited(ctx: StateContext<MyStateModel>, emitted :Emitted<MyAction, Item[]>){
  ctx.patchState({ items: emmited.payload })
}

Firestore plugin in action

Now that we understand how the plugin works, let's go over a complete implementation of the @ngxs-labs/firestore-plugin.

Before going through these steps you'll need to add @ngxs/store and @angular/fire to your app, which is out of the scope of this post.

The following example is a basic app that uses Firestore as the backend, and performs common operations such as read items, create, update and delete. We'll implement each of these operations and discuss them as we go through them.

The complete example can be found in stackblitz

First, install the library:

npm install @ngxs-labs/firestore-plugin

Next, we'll add it to the AppModule

@NgModule({
    //...
    imports: [
        //...
        // here goes all other Ngxs and AngularFire imports
        NgxsFirestoreModule.forRoot()
    ]
})
export class AppModule {
}

Next, we create our @State

import {
  NgxsFirestoreConnect,
  Emitted,
  StreamEmitted,
} from '@ngxs-labs/firestore-plugin';
//..

export interface Race {
  id: string;
  title: string;
  description: string;
  name: string;
}

export interface RacesStateModel {
  races: Race[];
}

@State<RacesStateModel>({
  name: 'races',
  defaults: {
    races: []
  }
})
@Injectable()
export class RacesState implements NgxsOnInit {
  @Selector() static races(state: RacesStateModel) {
    return state.races;
  }

  constructor(private racesFS: RacesFirestore, private ngxsFirestoreConnect: NgxsFirestoreConnect) {}

  ngxsOnInit(ctx: StateContext<RacesStateModel>) {
    this.ngxsFirestoreConnect.connect(RacesActions.GetAll, {
      to: () => this.racesFS.collection$()
    });
  }

  @Action(StreamEmitted(RacesActions.GetAll))
  getAllEmitted(ctx: StateContext<RacesStateModel>, { action, payload }: Emitted<RacesActions.Get, Race[]>) {
    ctx.setState(patch({ races: payload }));
  }
}

The firestore service

import { NgxsFirestore } from '@ngxs-labs/firestore-plugin';
//...

@Injectable({
  providedIn: 'root'
})
export class RacesFirestore extends NgxsFirestore<Race> {
  protected path = 'races';
}

And our component

//...
<button class="btn btn-primary"
        (click)="getAll()">Get All</button>
//...
<div *ngFor="let race of races$ | async"
     class="card mr-1 mb-1 d-inline-flex"
     style="width: 12rem;">
  <div class="card-body">
    <h5 class="card-title">{{ race.id }}</h5>
    <p class="card-text">{{ race.name }}</p>
    <p class="card-text">{{ race.description }}</p>

    <button (click)="update(race)"
            class="mr-1">Update</button>
    <button (click)="delete(race.id)">Delete</button>
  </div>
</div>
@Component({
  //...
})
export class ListComponent implements OnInit, OnDestroy {
  races$ = this.store.select(RacesState.races);
  constructor(private store: Store) {}

  getAll() {
    this.store.dispatch(new RacesActions.GetAll());
  }  
}

Let's recap what we have so far, we connected a query to get all items and list them.

When we click the "Get All" button, the query will be executed, getting all items in the collection. Every change on Firestore DB will be immediately streamed to the component.

Here's how it would look:

Get All Items
Get All Items

Now, let's add a "Create" button and a counter of all the items

//...
<button class="btn btn-primary mr-1"
                (click)="create()">Create</button>
//...
<div>
   <h5>Total: {{ total$ | async }}</h5>
</div>
//...
total$ = this.races$.pipe(map((races) => races.length));
//...
create() {
  this.store.dispatch(new RacesActions.Create({
      id: 'test-id',
      name: 'Test',
      title: 'Test Title',
      description: 'Test description',
  }));
}

We add the create action to the state

export namespace RacesActions {
  // ...
  export class Create {
    public static readonly type = '[Races] Create';
    constructor(public payload: RacesActionsPayloads.Create) {}
  }
}
//...
  @Action(RacesActions.Create)
  create({ patchState, dispatch }: StateContext<RacesStateModel>, { payload }: RacesActions.Create) {
    return this.racesFS.create$(payload.id, payload);
  }

When we hit "Create", count goes up, and the item gets immediately streamed to the component.

Notice that all of this happened without us needing to re-fetch any data. Since we connected the query to the store, every emission is streamed automatically to the component

Create item
Create item

Now let's take a look at some other cool features.

Track all active connections

The plugin comes with a @Selector to track all active connections in the app. This becomes very helpful for debugging and monitoring performance and how many connections are open on a given moment.

import { ngxsFirectoreConnections } from '@ngxs-labs/firestore-plugin';

//...
 ngxsFirestoreState$ = this.store.select(ngxsFirectoreConnections);

The selector outputs all active connections and emissions as we can see in the following image.

ngxsFirectoreConnections
ngxsFirectoreConnections

Connecting with single-document based query

Let's say we want to connect to specific documents from a collection. We might retrieve some document ids initially and we now want to query those documents. Let's take a look at how we can do that. First we'll add a new button to get some specific items from the races collection.

<button [disabled]="gettingSingle$ | async"
                class="btn btn-primary"
                (click)="get()">Get Single</button>
get() {
const ids = [your ids go here];
    ids.forEach((id) => this.store.dispatch(new RacesActions.Get(id)));
}

We add the new action

export namespace RacesActions {
  // ...
  export class Get {
    public static readonly type = '[Races] Get';
    constructor(public payload: string) {}
  }
}

And setup query connection in the state

//...
ngxsOnInit(ctx: StateContext<RacesStateModel>) {
  //...
  this.ngxsFirestoreConnect.connect(RacesActions.Get, {
      to: (action) => this.racesFS.doc$(action.payload),
      trackBy: (action) => action.payload
    });
}

  @Action(StreamEmitted(RacesActions.Get))
  getEmitted(ctx: StateContext<RacesStateModel>, { action, payload }: Emitted<RacesActions.Get, Race>) {
    if (payload) {
      ctx.setState(
        patch<RacesStateModel>({
          races: iif(
            (races) => !!races.find((race) => race.id === payload.id),
            updateItem((race) => race.id === payload.id, patch(payload)),
            insertItem(payload)
          )
        })
      );
    }
  }

Let's take a closer look how the connection is set up

  this.ngxsFirestoreConnect.connect(RacesActions.Get, {
      to: (action) => this.racesFS.doc$(action.payload),
      trackBy: (action) => action.payload
    });

As we saw before, to takes a function that returns an observable Firestore query. In this case we can see the function takes the action as param, this allows us to send the id as the action payload and use it to query for a specific doc.
Another important thing here is trackBy. This param allows us to tell the plugin how to track connection. If the trackBy id matches an existing connection, the plugin will not create a new subscription (because there's already an active one), otherwise it will start a new one.

Multiple connection to single documents
Multiple connection to single documents

StreamConnected and StreamDisconnected

In this tutorial we saw how StreamEmitted action helper allows us to hook into every change that occurs in a connected query and update the state as we need to. But this is not the only hook the plugin supports.

We can also hook into the StreamConnected and StreamDisconnected events.

  • StreamConnected will dispatch on first emission only and
  • StreamDisconnected when we explicitly disconnect from the stream. Both hooks allow us to respond to the origin action (with its payload, if present).
  @Action(StreamConnected(RacesActions.Get))
  getConnected(ctx: StateContext<RacesStateModel>, { action }: Connected<RacesActions.Get>) {
    console.log('[RacesActions.Get]  Connected');
  }
  @Action(StreamDisconnected(RacesActions.Get))
  getDisconnected(ctx: StateContext<RacesStateModel>, { action }: Disconnected<RacesActions.Get>) {
    console.log('[RacesActions.Get] Disconnected');
  }

Configure when the action finishes

When we set up the "connection" in the @State, there is another property we can configure. connectedActionFinishesOn lets us define when the action will actually complete.

As we mentioned before, when we set up the connection it doesn't execute it immediately. The query is executed once the connected action is dispatched.
With connectedActionFinishesOn we can control when the triggering Action completes, the options are FirstEmit and StreamCompleted. FirstEmit is the default and is pretty much self explanatory, once the firestore query emits the first result, the action will complete. On the other hand, StreamCompleted will cause the triggering action to complete when the connected stream completes (disconnecting a stream comes later in the article).

FirstEmit is very helpful if we want to display a "Loading..." message or disable a button while data is being fetched.

Let's see an example of how we can achieve this using another NGXS plugin @ngxs-labs/actions-executing.

First we set up the query connection:

this.ngxsFirestoreConnect.connect(RacesActions.GetAll, {
  to: () => this.racesFS.collection$(), 
  connectedActionFinishesOn: 'FirstEmit'
});

Next, in our component we define the loading$ observable selecting actionsExecuting.

import { actionsExecuting } from '@ngxs-labs/actions-executing';

//...
loading$ = this.store.select(actionsExecuting([RacesState.races]))
// we could also use @Select(RacesState.races) races$
races$ = this.store.select(RacesState.races);
this.store.dispatch(new RacesActions.GetAll());

Finally we display Loading... using | async

<div *ngIf="loading$ | async; else loaded">
  //...
</div>
<ng-template #loaded>
  //...
</ng-template>
Display Loading... while fetching data
Display Loading... while fetching data

Disconnect from stream

So far we've discussed how to connect and update the state. Once we dispatch the action, the stream gets subscribed and emits data as it comes in. But what if we want to stop receiving data from that stream? Well, we can dispatch a Disconnect action to do that.

Update item while action is connected
Update item while action is connected
Disconnected action doesn’t emit new results
Disconnected action doesn’t emit new results
// Connect
this.store.dispatch(new RacesActions.Get({id}));

// Disconnect
this.store.dispatch(new Disconnect(new RacesActions.Get({id})));

As we can see, while the action is connected, data will be emitted and displayed in our component. Once we disconnect the action, we will no longer listen to changes, and therefore not reflect the changes in the component.

Note that this connection uses the trackBy option, therefore an action with the same id can be used to disconnect. Alternatively you could keep a reference to the original action and use it to disconnect.

If we want to connect back to the query, we just need to dispatch the Action again.

Conclusion

In this article we covered how we can connect Firestore queries with NGXS using the @ngxs-labs/firestore-plugin. We explained how the plugin works in detail and demonstrated some basic and more advanced examples of how we can use it.
We also showed some nice features the plugin includes such as tracking the active connections and various event hooks.

I hope that you enjoyed this article, and that you try out this plugin for yourself!

Please add your thoughts and feedback in the github project or share your experiences on the NGXS Slack.

Special thanks to Mark Whitfeld for reviewing and helping me with this article.