Cover image
Back-end
15 minute read

State Management in Angular Using Firebase

Without proper state management, your Angular app will become a UX nightmare. But even with that solved, persisting state across sessions and devices can be tricky. This tutorial shows how to leapfrog that challenge using Firebase.

State management is a very important piece of architecture to consider when developing a web app.

In this tutorial, we’ll go over a simple approach to manage state in an Angular application that uses Firebase as its back end.

We’ll go over some concepts such as state, stores, and services. Hopefully, this will help you get a better grasp of these terms and also better understand other state management libraries such as NgRx and NgXs.

We’ll build an employee admin page in order to cover some different state management scenarios and the approaches that can handle them.

Components, Services, Firestore, and State Management in Angular

On a typical Angular application we have components and services. Usually, components will serve as the view template. Services will contain business logic and/or communicate with external APIs or other services to complete actions or retrieve data.

Components connect to services, which connect to other services or HTTP APIs.

Components will usually display data and allow users to interact with the app to execute actions. While doing this, data may change and the app reflects those changes by updating the view.

Angular’s change detection engine takes care of checking when a value in a component bound to the view has changed and updates the view accordingly.

As the app grows, we’ll start having more and more components and services. Often understanding how data is changing and tracking where that happens can be tricky.

Angular and Firebase

When we use Firebase as our back end, we are provided with a really neat API that contains most of the operations and functionality we need to build a real-time application.

@angular/fire is the official Angular Firebase library. It’s a layer on top of the Firebase JavaScript SDK library that simplifies the use of the Firebase SDK in an Angular app. It provides a nice fit with Angular good practices such as using Observables for getting and displaying data from Firebase to our components.

Components subscribe to the Firebase JavaScript API via @angular/fire using Observables.

Stores and State

We can think of “state” as being the values displayed at any given point in time in the app. The store is simply the holder of that application state.

State can be modeled as a single plain object or a series of them, reflecting the values of the application.

A store holding state, which has an example object with some simple key-value pairs for name, city, and country.

Angular/Firebase Sample App

Let’s build it: First, we’ll create a basic app scaffold using Angular CLI, and connect it with a Firebase project.

$ npm install -g @angular/cli
$ ng new employees-admin`

Would you like to add Angular routing? Yes
Which stylesheet format would you like to use? SCSS

$ cd employees-admin/
$ npm install bootstrap # We'll add Bootstrap for the UI

And, on styles.scss:

// ...
@import "~bootstrap/scss/bootstrap";

Next, we’ll install @angular/fire:

npm install firebase @angular/fire

Now, we’ll create a Firebase Project at the Firebase console.

The Firebase console's "Add a project" dialog.

Then we’re ready to create a Firestore database.

For this tutorial, I’ll start in test mode. If you plan to release to production, you should enforce rules to forbid inappropriate access.

The "Security rules for Cloud Firestore" dialog, with "Start in test mode" selected instead of "Start in locked mode."

Go to Project Overview → Project Settings, and copy the Firebase web config to your local environments/environment.ts.

Empty apps listing for the new Firebase project.

export const environment = {
    production: false,
        firebase: {
        apiKey: "<api-key>",
        authDomain: "<auth-domain>",
        databaseURL: "<database-url>",
        projectId: "<project-id>",
        storageBucket: "<storage-bucket>",
        messagingSenderId: "<messaging-sender-id>"
    }
};

At this point, we have the basic scaffold in place for our app. If we ng serve, we’ll get:

The Angular scaffold, saying "Welcome to employees-admin!"

Firestore and Store Base Classes

We’ll create two generic abstract classes, which we’ll then type and extend from to build our services.

Generics let you write behavior without a bound type. This adds reusability and flexibility to your code.

Generic Firestore Service

In order to take advantage of TypeScript generics, what we’ll do is create a base generic wrapper for the @angular/fire firestore service.

Let’s create app/core/services/firestore.service.ts.

Here’s the code:

import { Inject } from "@angular/core";
import { AngularFirestore, QueryFn } from "@angular/fire/firestore";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { environment } from "src/environments/environment";

    export abstract class FirestoreService<T> {

    protected abstract basePath: string;

    constructor(
        @Inject(AngularFirestore) protected firestore: AngularFirestore,
    ) {

    }

    doc$(id: string): Observable<T> {
        return this.firestore.doc<T>(`${this.basePath}/${id}`).valueChanges().pipe(
            tap(r => {
                if (!environment.production) {
                    console.groupCollapsed(`Firestore Streaming [${this.basePath}] [doc$] ${id}`)
                    console.log(r)
                    console.groupEnd()
                }
            }),
        );
    }

    collection$(queryFn?: QueryFn): Observable<T[]> {
        return this.firestore.collection<T>(`${this.basePath}`, queryFn).valueChanges().pipe(
            tap(r => {
                if (!environment.production) {
                    console.groupCollapsed(`Firestore Streaming [${this.basePath}] [collection$]`)
                    console.table(r)
                    console.groupEnd()
                }
            }),
        );
    }

    create(value: T) {
        const id = this.firestore.createId();
        return this.collection.doc(id).set(Object.assign({}, { id }, value)).then(_ => {
            if (!environment.production) {
                console.groupCollapsed(`Firestore Service [${this.basePath}] [create]`)
                console.log('[Id]', id, value)
                console.groupEnd()
            }
        })
    }

    delete(id: string) {
        return this.collection.doc(id).delete().then(_ => {
            if (!environment.production) {
                console.groupCollapsed(`Firestore Service [${this.basePath}] [delete]`)
                console.log('[Id]', id)
                console.groupEnd()
            }
        })
    }

    private get collection() {
        return this.firestore.collection(`${this.basePath}`);
    }
}

This abstract class will work as a generic wrapper for our Firestore services.

This should be the only place where we should inject AngularFirestore. This will minimize the impact when the @angular/fire library gets updated. Also, if at some point we want to change the library, we will only need to update this class.

I added doc$, collection$, create, and delete. They wrap @angular/fire’s methods and provide logging when Firebase streams data—this will become very handy for debugging—and after an object is created or deleted.

Generic Store Service

Our generic store service will be built using RxJS’ BehaviorSubject. BehaviorSubject lets subscribers get the last emitted value as soon they subscribe. In our case, this is helpful because we’ll be able to begin the store with an initial value for all our components when they subscribe to the store.

The store will have two methods, patch and set. (We’ll create get methods later.)

Let’s create app/core/services/store.service.ts:

import { BehaviorSubject, Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

export abstract class StoreService<T> {

    protected bs: BehaviorSubject<T>;
    state$: Observable<T>;
    state: T;
    previous: T;

    protected abstract store: string;

    constructor(initialValue: Partial<T>) {
        this.bs = new BehaviorSubject<T>(initialValue as T);
        this.state$ = this.bs.asObservable();

        this.state = initialValue as T;
        this.state$.subscribe(s => {
            this.state = s
        })
    }

    patch(newValue: Partial<T>, event: string = "Not specified") {
        this.previous = this.state
        const newState = Object.assign({}, this.state, newValue);
        if (!environment.production) {
            console.groupCollapsed(`[${this.store} store] [patch] [event: ${event}]`)
            console.log("change", newValue)
            console.log("prev", this.previous)
            console.log("next", newState)
            console.groupEnd()
        }
        this.bs.next(newState)
    }

    set(newValue: Partial<T>, event: string = "Not specified") {
        this.previous = this.state
        const newState = Object.assign({}, newValue) as T;
        if (!environment.production) {
            console.groupCollapsed(`[${this.store} store] [set] [event: ${event}]`)
            console.log("change", newValue)
            console.log("prev", this.previous)
            console.log("next", newState)
            console.groupEnd()
        }
        this.bs.next(newState)
    }
}

As a generic class, we’ll defer typing until it’s properly extended.

The constructor will receive the initial value of type Partial<T>. This will allow us to only apply values to some properties of the state. The constructor will also subscribe to the internal BehaviorSubject emissions and keep the internal state updated after every change.

patch() will receive the newValue of type Partial<T> and will merge it with the current this.state value of the store. Finally, we next() the newState and emit the new state to all of the store subscribers.

set() works very similarly, only that instead of patching the state value, it will set it to the newValue it received.

We’ll log the previous and next values of the state as changes occur, which will help us debug and easily track state changes.

Putting It All Together

Okay, let’s see all this in action. What we’ll do is create an employees page, which will contain a list of employees, plus a form to add new employees.

Let’s update app.component.html to add a simple navigation bar:

<nav class="navbar navbar-expand-lg navbar-light bg-light mb-3">
    <span class="navbar-brand mb-0 h1">Angular + Firebase + State Management</span>
    <ul class="navbar-nav mr-auto">
        <li class="nav-item" [routerLink]="['/employees']" routerLinkActive="active">
            <a class="nav-link">Employees</a>
        </li>
    </ul>
</nav>
<router-outlet></router-outlet>

Next, we’ll create a Core module:

ng g m Core

In core/core.module.ts, we’ll add the modules required for our app:

// ...
import { AngularFireModule } from '@angular/fire'
import { AngularFirestoreModule } from '@angular/fire/firestore'
import { environment } from 'src/environments/environment';
import { ReactiveFormsModule } from '@angular/forms'

@NgModule({
    // ...
    imports: [
        // ...
        AngularFireModule.initializeApp(environment.firebase),
        AngularFirestoreModule,
        ReactiveFormsModule,
    ],
    exports: [
        CommonModule,
        AngularFireModule,
        AngularFirestoreModule,
        ReactiveFormsModule
    ]
})
export class CoreModule { }

Now, let’s create the employees page, starting with the Employees module:

ng g m Employees --routing

In employees-routing.module.ts, let’s add the employees route:

// ...
import { EmployeesPageComponent } from './components/employees-page/employees-page.component';

// ...
const routes: Routes = [
    { path: 'employees', component: EmployeesPageComponent }
];
// ...

And in employees.module.ts, we’ll import ReactiveFormsModule:

// ...
import { ReactiveFormsModule } from '@angular/forms';
// ...

@NgModule({
    // ...
    imports: [
        // ...   
        ReactiveFormsModule
    ]
})
export class EmployeesModule { }

Now, let’s add these two modules in the app.module.ts file:

// ...
import { EmployeesModule } from './employees/employees.module';
import { CoreModule } from './core/core.module';

imports: [
    // ...
    CoreModule,
    EmployeesModule
],

Finally, let’s create the actual components of our employees page, plus the corresponding model, service, store, and state.

ng g c employees/components/EmployeesPage
ng g c employees/components/EmployeesList
ng g c employees/components/EmployeesForm

For our model, we’ll need a file called models/employee.ts:

export interface Employee {
    id: string;
    name: string;
    location: string;
    hasDriverLicense: boolean;
}

Our service will live in a file called employees/services/employee.firestore.ts. This service will extend the generic FirestoreService<T> created before, and we’ll just set the basePath of the Firestore collection:

import { Injectable } from '@angular/core';
import { FirestoreService } from 'src/app/core/services/firestore.service';
import { Employee } from '../models/employee';

@Injectable({
    providedIn: 'root'
})
export class EmployeeFirestore extends FirestoreService<Employee> {

    protected basePath: string = 'employees';

}

Then we’ll create the file employees/states/employees-page.ts. This will serve as the state of the employees page:

import { Employee } from '../models/employee';
export interface EmployeesPage {

    loading: boolean;
    employees: Employee[];
    formStatus: string;

}

The state will have a loading value that determines whether to display a loading message on the page, the employees themselves, and a formStatus variable to handle the status of the form (e.g. Saving or Saved.)

We’ll need a file at employees/services/employees-page.store.ts. Here we’ll extend the StoreService<T> created before. We’ll set the store name, which will be used to identify it when debugging.

This service will initialize and hold the state of the employees page. Note that the constructor calls super() with the initial state of the page. In this case, we’ll initialize the state with loading=true and an empty array of employees.

import { EmployeesPage } from '../states/employees-page';
import { StoreService } from 'src/app/core/services/store.service';
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root'
})
export class EmployeesPageStore extends StoreService<EmployeesPage> {
    protected store: string = 'employees-page';

    constructor() {
        super({
            loading: true,
            employees: [],
        })
    }
}

Now let’s create EmployeesService to integrate EmployeeFirestore and EmployeesPageStore:

ng g s employees/services/Employees

Note that we are injecting the EmployeeFirestore and EmployeesPageStore in this service. This means that the EmployeesService will contain and coordinate calls to Firestore and the store to update the state. This will help us create a single API for components to call.

import { EmployeesPageStore } from './employees-page.store';
import { EmployeeFirestore } from './employee.firestore';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Employee } from '../models/employee';
import { tap, map } from 'rxjs/operators';

@Injectable({
    providedIn: 'root'
})
export class EmployeesService {

    constructor(
        private firestore: EmployeeFirestore,
        private store: EmployeesPageStore
    ) {
        this.firestore.collection$().pipe(
            tap(employees => {
                this.store.patch({
                    loading: false,
                    employees,        
                }, `employees collection subscription`)
            })
        ).subscribe()
    }

    get employees$(): Observable<Employee[]> {
        return this.store.state$.pipe(map(state => state.loading
            ? []
            : state.employees))
    }

    get loading$(): Observable<boolean> {
        return this.store.state$.pipe(map(state => state.loading))
    }

    get noResults$(): Observable<boolean> {
        return this.store.state$.pipe(
            map(state => {
                return !state.loading
                    && state.employees
                    && state.employees.length === 0
            })
        )
    }

    get formStatus$(): Observable<string> {
        return this.store.state$.pipe(map(state => state.formStatus))
    }

    create(employee: Employee) {
        this.store.patch({
            loading: true,
            employees: [],
            formStatus: 'Saving...'
        }, "employee create")
        return this.firestore.create(employee).then(_ => {
            this.store.patch({
                formStatus: 'Saved!'
            }, "employee create SUCCESS")
            setTimeout(() => this.store.patch({
                formStatus: ''
            }, "employee create timeout reset formStatus"), 2000)
        }).catch(err => {
            this.store.patch({
                loading: false,
                formStatus: 'An error ocurred'
            }, "employee create ERROR")
        })
    }

    delete(id: string): any {
        this.store.patch({ loading: true, employees: [] }, "employee delete")
        return this.firestore.delete(id).catch(err => {
            this.store.patch({
                loading: false,
                formStatus: 'An error ocurred'
            }, "employee delete ERROR")
        })
    }
}

Let’s take a look at how the service will work.

In the constructor, we’ll subscribe to the Firestore employees collection. As soon as Firestore emits data from the collection, we’ll update the store, setting loading=false and employees with Firestore’s returned collection. Since we have injected EmployeeFirestore, the objects returned from Firestore are typed to Employee, which enables more IntelliSense features.

This subscription will be alive while the app is active, listening for all changes and updating the store every time Firestore streams data.

this.firestore.collection$().pipe(
    tap(employees => {
        this.store.patch({
        loading: false,
        employees,        
        }, `employees collection subscription`)
    })
).subscribe()

The employees$() and loading$() functions will select the piece of state we want to later use on the component. employees$() will return an empty array when the state is loading. This will allow us to display proper messaging on the view.

get employees$(): Observable<Employee[]> {
    return this.store.state$.pipe(map(state => state.loading ? [] : state.employees))
}

get loading$(): Observable<boolean> {
    return this.store.state$.pipe(map(state => state.loading))
}

Okay, so now we have all the services ready, and we can build our view components. But before we do that, a quick refresher might come in handy…

RxJs Observables and the async Pipe

Observables allow subscribers to receive emissions of data as a stream. This, in combination with the async pipe, can very powerful.

The async pipe takes care of subscribing to an Observable and updating the view when new data is emitted. More importantly, it automatically unsubscribes when the component is destroyed, protecting us from memory leaks.

You can read more about Observables and RxJs library in general in the official docs.

Creating the View Components

In employees/components/employees-page/employees-page.component.html, we’ll put this code:

<div class="container">
    <div class="row">
        <div class="col-12 mb-3">
            <h4>
                Employees
            </h4>
        </div>
    </div>
    <div class="row">
        <div class="col-6">
            <app-employees-list></app-employees-list>
        </div>
        <div class="col-6">
            <app-employees-form></app-employees-form>
        </div>
    </div>
</div>

Likewise, employees/components/employees-list/employees-list.component.html will have this, using the async pipe technique mentioned above:

<div *ngIf="loading$ | async">
    Loading...
</div>
<div *ngIf="noResults$ | async">
    No results
</div>
<div class="card bg-light mb-3" style="max-width: 18rem;" *ngFor="let employee of employees$ | async">
    <div class="card-header">{{employee.location}}</div>
    <div class="card-body">
        <h5 class="card-title">{{employee.name}}</h5>
        <p class="card-text">{{employee.hasDriverLicense ? 'Can drive': ''}}</p>
        <button (click)="delete(employee)" class="btn btn-danger">Delete</button>
    </div>
</div>

But in this case we’ll need some TypeScript code for the component, too. The file employees/components/employees-list/employees-list.component.ts will need this:

import { Employee } from '../../models/employee';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { EmployeesService } from '../../services/employees.service';

@Component({
    selector: 'app-employees-list',
    templateUrl: './employees-list.component.html',
    styleUrls: ['./employees-list.component.scss']
})
export class EmployeesListComponent implements OnInit {
    loading$: Observable<boolean>;
    employees$: Observable<Employee[]>;
    noResults$: Observable<boolean>;

    constructor(
        private employees: EmployeesService
    ) {}

    ngOnInit() {
        this.loading$ = this.employees.loading$;
        this.noResults$ = this.employees.noResults$;
        this.employees$ = this.employees.employees$;
    }

    delete(employee: Employee) {
        this.employees.delete(employee.id);
    }

}

So, going to the browser, what we’ll have now is:

An empty employees list, and the message "employees-form works!"

And the console will have the following output:

Patch events showing changes with before and after values.

Looking at this, we can tell that Firestore streamed the employees collection with empty values, and the employees-page store was patched, setting loading from true to false.

OK, let’s build the form to add new employees to Firestore:

The Employees Form

In employees/components/employees-form/employees-form.component.html we’ll add this code:

<form [formGroup]="form" (ngSubmit)="submit()">
    <div class="form-group">
        <label for="name">Name</label>
        <input type="string" class="form-control" id="name"
          formControlName="name" [class.is-invalid]="isInvalid('name')">
        <div class="invalid-feedback">
            Please enter a Name.
        </div>
    </div>
    <div class="form-group">
        <select class="custom-select" formControlName="location"
          [class.is-invalid]="isInvalid('location')">
            <option value="" selected>Choose location</option>
            <option *ngFor="let loc of locations" [ngValue]="loc">{{loc}}</option>
        </select>
        <div class="invalid-feedback">
            Please select a Location.
        </div>
    </div>
    <div class="form-group form-check">
        <input type="checkbox" class="form-check-input" id="hasDriverLicense"
          formControlName="hasDriverLicense">
        <label class="form-check-label" for="hasDriverLicense">Has driver license</label>
    </div>
    <button [disabled]="form.invalid" type="submit" class="btn btn-primary d-inline">Add</button>
    <span class="ml-2">{{ status$ | async }}</span>
</form>

The corresponding TypeScript code will live in employees/components/employees-form/employees-form.component.ts:

import { EmployeesService } from './../../services/employees.service';
import { AngularFirestore } from '@angular/fire/firestore';
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { Observable } from 'rxjs';

@Component({
    selector: 'app-employees-form',
    templateUrl: './employees-form.component.html',
    styleUrls: ['./employees-form.component.scss']
})
export class EmployeesFormComponent implements OnInit {

    form: FormGroup = new FormGroup({
        name: new FormControl('', Validators.required),
        location: new FormControl('', Validators.required),
        hasDriverLicense: new FormControl(false)
    });

    locations = [
        'Rosario',
        'Buenos Aires',
        'Bariloche'
    ]

    status$: Observable < string > ;

    constructor(
        private employees: EmployeesService
    ) {}

    ngOnInit() {
        this.status$ = this.employees.formStatus$;
    }

    isInvalid(name) {
        return this.form.controls[name].invalid
           && (this.form.controls[name].dirty || this.form.controls[name].touched)
    }

    async submit() {
        this.form.disable()
        await this.employees.create({ ...this.form.value
        })
        this.form.reset()
        this.form.enable()
    }

}

The form will call the create() method of EmployeesService. Right now the page looks like this:

The same empty employees list as before, this time with a form for adding a new employee.

Let’s take a look at what happens when we add a new employee.

Adding a New Employee

After adding a new employee, we’ll see the following get output to the console:

Patch events intermixed with Firestore events, numbered one through six (local creation, Firestore collection streaming, local collection subscription, Firestore creation, local creation success, and local creation timeout form status reset.)

These are all the events that get triggered when adding a new employee. Let’s take a closer look.

When we call create() we’ll execute the following code, setting loading=true, formStatus='Saving...' and the employees array to empty ((1) in the above image).

this.store.patch({
    loading: true,
    employees: [],
    formStatus: 'Saving...'
}, "employee create")
return this.firestore.create(employee).then(_ => {
    this.store.patch({
        formStatus: 'Saved!'
    }, "employee create SUCCESS")
    setTimeout(() => this.store.patch({
        formStatus: ''
    }, "employee create timeout reset formStatus"), 2000)
}).catch(err => {
    this.store.patch({
        loading: false,
        formStatus: 'An error ocurred'
    }, "employee create ERROR")
})

Next, we are calling the base Firestore service to create the employee, which logs (4). On the promise callback, we set formStatus='Saved!' and log (5). Finally, we set a timeout to set formStatus back to empty, logging (6).

Log events (2) and (3) are the events triggered by the Firestore subscription to the employees collection. When the EmployeesService is instantiated, we subscribe to the collection and receive the collection upon every change that happens.

This sets a new state to the store with loading=false by setting the employees array to the employees coming from Firestore.

If we expand the log groups, we’ll see detailed data of every event and update of the store, with the previous value and next, which is useful for debugging.

The previous log output with all state management detail expanded.

This is how the page looks like after adding a new employee:

The employee list with an employee card in it, and the form still filled out from adding it.

Adding a Summary Component

Let’s say we now want to display some summary data on our page. Let’s say we want the total number of employees, how many are drivers, and how many are from Rosario.

We’ll start by adding the new state properties to the page state model in employees/states/employees-page.ts:

// ...
export interface EmployeesPage {

    loading: boolean;
    employees: Employee[];
    formStatus: string;

    totalEmployees: number;
    totalDrivers: number;
    totalRosarioEmployees: number;

}

And we’ll initialize them in the store in employees/services/emplyees-page.store.ts:

// ...
constructor() {
    super({
        loading: true,
        employees: [],
        totalDrivers: 0,
        totalEmployees: 0,
        totalRosarioEmployees: 0
    })
}
// ...

Next, we’ll calculate the values for the new properties and add their respective selectors in the EmployeesService:

// ...

this.firestore.collection$().pipe(
    tap(employees => {
        this.store.patch({
            loading: false,
            employees,
            totalEmployees: employees.length,
            totalDrivers: employees.filter(employee => employee.hasDriverLicense).length,
            totalRosarioEmployees: employees.filter(employee => employee.location === 'Rosario').length,
        }, `employees collection subscription`)
    })
).subscribe()

// ...

get totalEmployees$(): Observable < number > {
    return this.store.state$.pipe(map(state => state.totalEmployees))
}

get totalDrivers$(): Observable < number > {
    return this.store.state$.pipe(map(state => state.totalDrivers))
}

get totalRosarioEmployees$(): Observable < number > {
    return this.store.state$.pipe(map(state => state.totalRosarioEmployees))
}

// ...

Now, let’s create the summary component:

ng g c employees/components/EmployeesSummary

We’ll put this in employees/components/employees-summary/employees-summary.html:

<p>
    <span class="font-weight-bold">Total:</span> {{total$ | async}} <br>
    <span class="font-weight-bold">Drivers:</span> {{drivers$ | async}} <br>
    <span class="font-weight-bold">Rosario:</span> {{rosario$ | async}} <br>
</p>

And in employees/components/employees-summary/employees-summary.ts:

import { Component, OnInit } from '@angular/core';
import { EmployeesService } from '../../services/employees.service';
import { Observable } from 'rxjs';

@Component({
    selector: 'app-employees-summary',
    templateUrl: './employees-summary.component.html',
    styleUrls: ['./employees-summary.component.scss']
})
export class EmployeesSummaryComponent implements OnInit {

    total$: Observable < number > ;
    drivers$: Observable < number > ;
    rosario$: Observable < number > ;

    constructor(
        private employees: EmployeesService
    ) {}

    ngOnInit() {
        this.total$ = this.employees.totalEmployees$;
        this.drivers$ = this.employees.totalDrivers$;
        this.rosario$ = this.employees.totalRosarioEmployees$;
    }

}

We’ll then add the component to employees/employees-page/employees-page.component.html:

// ...
<div class="col-12 mb-3">
    <h4>
        Employees
    </h4>
    <app-employees-summary></app-employees-summary>
</div>
// ...

The result is the following:

Employees page, now with a summary above the list, showing counts of total employees, those who are drivers, and those who are from Rosario.

In the console we have:

Console output showing a patch event changing the summary values.

The employees service calculates the total totalEmployees, totalDrivers, and totalRosarioEmployees on each emission and updates the state.

The full code of this tutorial is available on GitHub, and there’s also a live demo.

Managing Angular App State Using Observables… Check!

In this tutorial, we covered a simple approach for managing state in Angular apps using a Firebase back end.

This approach fits nicely with the Angular guidelines of using Observables. It also facilitates debugging by providing tracking for all updates to the app’s state.

The generic store service can also be used to manage state of apps that don’t use Firebase features, either to manage only the app’s data or data coming from other APIs.

But before you go applying this indiscriminately, one thing to consider is that EmployeesService subscribes to Firestore on the constructor and keeps listening while the app is active. This might be useful if we use the employees list on multiple pages on the app, to avoid getting data from Firestore when navigating between pages.

But this might not be the best option in other scenarios like if you just need to pull initial values once and then manually trigger reloads of data from Firebase. The bottom line is, it’s always important to understand your app’s requirements in order to choose better methods of implementation.


Further Reading on the Toptal Engineering Blog:

Understanding the basics

Angular (originally AngularJS) is a popular front-end framework for creating single-page applications (SPAs). It's open-source and backed by Google.

State management is about properly tracking variables in a web app. E.g. if a chat app user switched chatrooms, that's a change in state. If they then sent a message, but the sending feature was not aware of the earlier state change, it would send the message to the previous chatroom, resulting in a very poor UX.

Google's Firebase is an all-in-one mobile app development platform. It's well-known for its original real-time database offering, but nowadays includes integrated crash reporting, authentication, and asset hosting, among others.

Comments

Franco Battista
Nice post!
Franco Battista
Nice post!
Cristian Fuentes
Amazing!! Thanks a lot!
Cristian Fuentes
Amazing!! Thanks a lot!
Amir Fawzy
that's pretty stunning you explain state management and how to wire things up very well... thank you
Amir Fawzy
that's pretty stunning you explain state management and how to wire things up very well... thank you
Abdallah Fakry
What I've been searching for .... stunning
Joaquin Cid
thanks!
Saul Botier
This is amazing! It's going to come in handy very nicely. Well done.
Daniel Chu
Hi may I ask will this state management survive with a refresh?
Joaquin Cid
no, it will not
Daniel Chu
How will you suggest to solve that issue?
Joaquin Cid
well, you can enablePersistance on firestore settings to enable caching of docuemnts on the browser, that way you will read from local storage after refresh and then get updates from the server
Daniel Chu
tq!
Vladimíra Lokšová
Can you add a filter please?
Vladimíra Lokšová
I am using Angular 9 and there is no interface
Joaquin Cid
Hi! depending on the filter you want you have different options. But the main thing you'll need is to add a <code>Filter</code> object to the store, and then create another get accessor where you would pipe the results and filter accordingly <code> get fileredEmployees$(): Observable<Employee[]> { return combineLatest( this.employees$, this.filter$, ).pipe( map(([employees, filter])=> { return employees.filter(employee=>{ return employee.name === filter.name }) }) ) } </code> Hope this helps!
Joaquin Cid
Hi, sorry, i don't understand this
Joaquin Cid
i updated to ng 9 and works ok!, i also updated firebase btw, here's my dependencies list "dependencies": { "@angular/animations": "~9.1.0", "@angular/common": "~9.1.0", "@angular/compiler": "~9.1.0", "@angular/core": "~9.1.0", "@angular/fire": "^5.4.2", "@angular/forms": "~9.1.0", "@angular/platform-browser": "~9.1.0", "@angular/platform-browser-dynamic": "~9.1.0", "@angular/router": "~9.1.0", "bootstrap": "^4.1.3", "core-js": "^2.5.4", "firebase": "^5.11.1", "rxjs": "^6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" }, "devDependencies": { "@angular-devkit/build-angular": "~0.901.0", "@angular/cli": "~9.1.0", "@angular/compiler-cli": "~9.1.0", "@angular/language-service": "~9.1.0", "@types/node": "^12.11.1", "@types/jasmine": "~2.8.8", "@types/jasminewd2": "~2.0.3", "codelyzer": "^5.1.2", "jasmine-core": "~2.99.1", "jasmine-spec-reporter": "~4.2.1", "karma": "~3.0.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", "karma-jasmine": "~1.1.2", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.4.0", "ts-node": "~7.0.0", "tslint": "~5.11.0", "typescript": "~3.8.3" }
Obumuneme Nwabude
Thank you very much. When the angular app serves it just shows the navbar but doesn't show what comes from employees module I'm having similar errors in the console to what @Kevin Bloch got. First is core.js:6185 ERROR TypeError: Cannot read property 'collection' of undefined at EmployeeFirestore.collection$ (firestore.service.ts:30) at new EmployeesService (employees.service.ts:17) at Object.EmployeesService_Factory [as factory] (employees.service.ts:80) at R3Injector.hydrate (core.js:16865) at R3Injector.get (core.js:16617) at NgModuleRef$1.get (core.js:36024) at Object.get (core.js:33773) at getOrCreateInjectable (core.js:5805) at Module.ɵɵdirectiveInject (core.js:20861) at NodeInjectorFactory.EmployeesListComponent_Factory [as factory] (employees-list.component.ts:11) Second is main.ts:12 TypeError: Cannot read property 'collection' of undefined at EmployeeFirestore.collection$ (firestore.service.ts:30) at new EmployeesService (employees.service.ts:17) at Object.EmployeesService_Factory [as factory] (employees.service.ts:80) at R3Injector.hydrate (core.js:16865) at R3Injector.get (core.js:16617) at NgModuleRef$1.get (core.js:36024) at Object.get (core.js:33773) at getOrCreateInjectable (core.js:5805) at Module.ɵɵdirectiveInject (core.js:20861) at NodeInjectorFactory.EmployeesListComponent_Factory [as factory] (employees-list.component.ts:11) but my dependencies seem to be much more updated than yours, yet I have issues "dependencies": { "@angular/animations": "~9.1.0", "@angular/common": "~9.1.0", "@angular/compiler": "~9.1.0", "@angular/core": "~9.1.0", "@angular/fire": "^6.0.0", "@angular/forms": "~9.1.0", "@angular/platform-browser": "~9.1.0", "@angular/platform-browser-dynamic": "~9.1.0", "@angular/router": "~9.1.0", "bootstrap": "^4.4.1", "firebase": "^7.14.0", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" }, "devDependencies": { "@angular-devkit/build-angular": "~0.901.0", "@angular/cli": "~9.1.0", "@angular/compiler-cli": "~9.1.0", "@angular/language-service": "~9.1.0", "@types/node": "^12.11.1", "@types/jasmine": "~3.5.0", "@types/jasminewd2": "~2.0.3", "codelyzer": "^5.1.2", "jasmine-core": "~3.5.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~4.4.1", "karma-chrome-launcher": "~3.1.0", "karma-coverage-istanbul-reporter": "~2.1.0", "karma-jasmine": "~3.0.1", "karma-jasmine-html-reporter": "^1.4.2", "protractor": "~5.4.3", "ts-node": "~8.3.0", "tslint": "~6.1.0", "typescript": "~3.8.3" } please help
dvocke
I really enjoyed this! Very clear and clarifying many things. I'm currently trying to adapt it to my needs and run into firebase permission issues. The services (e.g. Employee.service.ts) subscribe to the firestore.collection$ observable in the constructor - even with the most basic firebase rule (request.auth != null) this leads to a permission error and the subscription fails. What would your suggestion be to solve this?
Joaquin Cid
have you added login to the app?
dvocke
yes, and if the "Normal" logging flow is followed everything works fine. The problem arises, when some user closes/opens the page or reloads it. Presumably because the subscriptions kicks in before the authState can be confirmed. Any ideas?
Joaquin Cid
yes, that's definitely the problem, what i usually do in these cases is set a Guard to the route, that waits until the authentication has been loaded, and the rest of components in the page loads, ensuring auth is already loaded
dvocke
ah, good thinking! Thanks
unicorn
has this been fixed? I am having the same error in Angular 10
Joaquin Cid
hi @disqus_TuGFfbOVry:disqus , just updated to ng10 and it works. here are my dependencies, you can also pull latest changes from the repo "dependencies": { "@angular/animations": "~10.0.2", "@angular/common": "~10.0.2", "@angular/compiler": "~10.0.2", "@angular/core": "~10.0.2", "@angular/fire": "^6.0.2", "@angular/forms": "~10.0.2", "@angular/platform-browser": "~10.0.2", "@angular/platform-browser-dynamic": "~10.0.2", "@angular/router": "~10.0.2", "bootstrap": "^4.1.3", "core-js": "^2.5.4", "firebase": "^7.15.5", "rxjs": "^6.6.0", "tslib": "^2.0.0", "zone.js": "~0.10.2" },
Phea Em
I follows every step and this happen core.js:6241 ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'collection' of undefined TypeError: Cannot read property 'collection' of undefined at EmployeeFirestore.collection$ (firestore.service.ts:30) at new EmployeesService (employees.service.ts:17) at Object.EmployeesService_Factory [as factory] (employees.service.ts:96) at R3Injector.hydrate (core.js:17206) at R3Injector.get (core.js:16956) at NgModuleRef$1.get (core.js:36342) at Object.get (core.js:33985) at getOrCreateInjectable (core.js:5848) at Module.ɵɵdirectiveInject (core.js:21116) at NodeInjectorFactory.EmployeesPageComponent_Factory [as factory] (employees-page.component.ts:10) at resolvePromise (zone-evergreen.js:798) at resolvePromise (zone-evergreen.js:750) at zone-evergreen.js:860 at ZoneDelegate.invokeTask (zone-evergreen.js:399) at Object.onInvokeTask (core.js:41645) at ZoneDelegate.invokeTask (zone-evergreen.js:398) at Zone.runTask (zone-evergreen.js:167) at drainMicroTaskQueue (zone-evergreen.js:569) at ZoneTask.invokeTask [as invoke] (zone-evergreen.js:484) at invokeTask (zone-evergreen.js:1621) defaultErrorLogger @ core.js:6241 handleError @ core.js:6294 next @ core.js:42627 schedulerFn @ core.js:37132 __tryOrUnsub @ Subscriber.js:183 next @ Subscriber.js:122 _next @ Subscriber.js:72 next @ Subscriber.js:49 next @ Subject.js:39 emit @ core.js:37092 (anonymous) @ core.js:41707 invoke @ zone-evergreen.js:364 run @ zone-evergreen.js:123 runOutsideAngular @ core.js:41501 onHandleError @ core.js:41704 handleError @ zone-evergreen.js:368 runGuarded @ zone-evergreen.js:136 api.microtaskDrainDone @ zone-evergreen.js:670 drainMicroTaskQueue @ zone-evergreen.js:576 invokeTask @ zone-evergreen.js:484 invokeTask @ zone-evergreen.js:1621 globalZoneAwareCallback @ zone-evergreen.js:1647
Joaquin
Hi! i think there might an issue with the <code>@Inject</code> of the AngularFirestore dependency, did you check that?
Naum Shapkarovski
Amazing! How could we add filters and infinite scroll to this implementation? Thank you
Joaquin
Hi! depending on the filter you want you have different options. But the main thing you'll need is to add a Filter object to the store, and then create another get accessor where you would pipe the results and filter accordingly get fileredEmployees$(): Observable<employee[]> { return combineLatest( this.employees$, this.filter$, ).pipe( map(([employees, filter])=> { return employees.filter(employee=>{ return employee.name === filter.name }) }) ) } Infinite scroll is a more complicated implementation, you can check out this post https://fireship.io/lessons... Hope this helps!
Thomas
I met the very same issue. I can't make @Inject work as expected. It's not instantiate hence, it remains undefined. I'm roughly familiar with Angular DI. So far, I can't make it work. Thanks for you article, it helped me.
Joaquin
did you add @Inject? you should also check AngularFireModule and AngularFirestoreModule are included in app.module
karim
Hello Joaquin , first thanks for this corse it's very intersting. I'm new in firebase and I used it as backend for my angular project. I need to add contact form and send email directly to my email address to my application but It's not worked correctly. I used as module first nodemailer and second emailjs but not working. Also I need to add notification system after adding new data to database but I don't know how. Any suggest ? Could you help me please ?
Jirka Zvěřina
Add <code>@Injectable()</code> to <i>firestore.service.ts</i>. It is missing in this article.
comments powered by Disqus