Gestión de estado en Angular usando Firebase
Publicado: 2022-03-11La gestión de estado es una pieza de arquitectura muy importante a tener en cuenta al desarrollar una aplicación web.
En este tutorial, repasaremos un enfoque simple para administrar el estado en una aplicación Angular que usa Firebase como back-end.
Repasaremos algunos conceptos como estado, tiendas y servicios. Con suerte, esto lo ayudará a comprender mejor estos términos y también a comprender mejor otras bibliotecas de administración de estado, como NgRx y NgXs.
Crearemos una página de administración de empleados para cubrir algunos escenarios de administración de estado diferentes y los enfoques que pueden manejarlos.
Componentes, servicios, Firestore y administración de estado en Angular
En una aplicación Angular típica tenemos componentes y servicios. Por lo general, los componentes servirán como plantilla de vista. Los servicios contendrán lógica comercial y/o se comunicarán con API externas u otros servicios para completar acciones o recuperar datos.
Los componentes generalmente mostrarán datos y permitirán a los usuarios interactuar con la aplicación para ejecutar acciones. Al hacer esto, los datos pueden cambiar y la aplicación refleja esos cambios al actualizar la vista.
El motor de detección de cambios de Angular se encarga de verificar cuándo un valor en un componente vinculado a la vista ha cambiado y actualiza la vista en consecuencia.
A medida que crezca la aplicación, comenzaremos a tener más y más componentes y servicios. A menudo, comprender cómo cambian los datos y rastrear dónde sucede eso puede ser complicado.
Angular y base de fuego
Cuando usamos Firebase como nuestro back-end, contamos con una API realmente ordenada que contiene la mayoría de las operaciones y la funcionalidad que necesitamos para crear una aplicación en tiempo real.
@angular/fire
es la biblioteca oficial de Angular Firebase. Es una capa sobre la biblioteca del SDK de JavaScript de Firebase que simplifica el uso del SDK de Firebase en una aplicación de Angular. Proporciona un buen ajuste con las buenas prácticas de Angular, como el uso de Observables para obtener y mostrar datos de Firebase a nuestros componentes.
Tiendas y Estado
Podemos pensar en "estado" como los valores que se muestran en cualquier momento dado en la aplicación. La tienda es simplemente el titular de ese estado de aplicación.
El estado se puede modelar como un solo objeto plano o una serie de ellos, reflejando los valores de la aplicación.
Aplicación de muestra de Angular/Firebase
Vamos a construirlo: Primero, crearemos un andamio de aplicación básico usando Angular CLI y lo conectaremos con un proyecto de Firebase.
$ 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
Y, en styles.scss
:
// ... @import "~bootstrap/scss/bootstrap";
A continuación, instalaremos @angular/fire
:
npm install firebase @angular/fire
Ahora, crearemos un proyecto de Firebase en la consola de Firebase.
Entonces estamos listos para crear una base de datos de Firestore.
Para este tutorial, comenzaré en modo de prueba. Si planea lanzar a producción, debe hacer cumplir las reglas para prohibir el acceso inapropiado.
Vaya a Descripción general del proyecto → Configuración del proyecto y copie la configuración web de Firebase en su environments/environment.ts
local.
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>" } };
En este punto, tenemos el andamio básico para nuestra aplicación. Si ng serve
, obtendremos:
Clases base de Firestore y Store
Crearemos dos clases abstractas genéricas, que luego escribiremos y extenderemos para construir nuestros servicios.
Los genéricos le permiten escribir comportamiento sin un tipo vinculado. Esto agrega reutilización y flexibilidad a su código.
Servicio Firestore genérico
Para aprovechar los genéricos de TypeScript, lo que haremos será crear un contenedor genérico básico para el servicio @angular/fire
firestore
.
Vamos a crear app/core/services/firestore.service.ts
.
Aquí está el código:
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}`); } }
Esta abstract class
funcionará como un envoltorio genérico para nuestros servicios de Firestore.
Este debería ser el único lugar donde deberíamos inyectar AngularFirestore
. Esto minimizará el impacto cuando se actualice la biblioteca @angular/fire
. Además, si en algún momento queremos cambiar la biblioteca, solo necesitaremos actualizar esta clase.
Agregué doc$
, collection$
, create
y delete
. Envuelven los métodos de @angular/fire
y proporcionan registro cuando Firebase transmite datos (esto será muy útil para la depuración) y después de que se crea o elimina un objeto.
Servicio de Tienda Genérica
Nuestro servicio de tienda genérica se creará utilizando BehaviorSubject
de RxJS. BehaviorSubject
permite a los suscriptores obtener el último valor emitido tan pronto como se suscriben. En nuestro caso, esto es útil porque podremos comenzar la tienda con un valor inicial para todos nuestros componentes cuando se suscriban a la tienda.
La tienda tendrá dos métodos, patch
y set
. (Crearemos métodos get
más adelante).
Vamos a crear 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) } }
Como clase genérica, pospondremos la escritura hasta que se extienda correctamente.
El constructor recibirá el valor inicial de tipo Partial<T>
. Esto nos permitirá aplicar valores solo a algunas propiedades del estado. El constructor también se suscribirá a las emisiones internas de BehaviorSubject
y mantendrá el estado interno actualizado después de cada cambio.
patch()
recibirá el newValue
de tipo Partial<T>
y lo combinará con el valor actual de this.state
de la tienda. Finalmente, usamos next()
the newState
y emitimos el nuevo estado a todos los suscriptores de la tienda.
set()
funciona de manera muy similar, solo que en lugar de parchear el valor del estado, lo establecerá en el newValue
que recibió.
Registraremos los valores anterior y siguiente del estado a medida que se produzcan los cambios, lo que nos ayudará a depurar y rastrear fácilmente los cambios de estado.
Poniendolo todo junto
Bien, veamos todo esto en acción. Lo que haremos será crear una página de empleados, que contendrá una lista de empleados, además de un formulario para agregar nuevos empleados.
app.component.html
para agregar una barra de navegación simple:
<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>
A continuación, crearemos un módulo Core:
ng gm Core
En core/core.module.ts
, agregaremos los módulos necesarios para nuestra aplicación:
// ... 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 { }
Ahora, creemos la página de empleados, comenzando con el módulo Empleados:
ng gm Employees --routing
En employees-routing.module.ts
, agreguemos la ruta de los employees
:
// ... import { EmployeesPageComponent } from './components/employees-page/employees-page.component'; // ... const routes: Routes = [ { path: 'employees', component: EmployeesPageComponent } ]; // ...
Y en employees.module.ts
, importaremos ReactiveFormsModule
:
// ... import { ReactiveFormsModule } from '@angular/forms'; // ... @NgModule({ // ... imports: [ // ... ReactiveFormsModule ] }) export class EmployeesModule { }
Ahora, agreguemos estos dos módulos en el archivo app.module.ts
:
// ... import { EmployeesModule } from './employees/employees.module'; import { CoreModule } from './core/core.module'; imports: [ // ... CoreModule, EmployeesModule ],
Finalmente, creemos los componentes reales de nuestra página de empleados, además del modelo, servicio, tienda y estado correspondientes.
ng gc employees/components/EmployeesPage ng gc employees/components/EmployeesList ng gc employees/components/EmployeesForm
Para nuestro modelo, necesitaremos un archivo llamado models/employee.ts
:
export interface Employee { id: string; name: string; location: string; hasDriverLicense: boolean; }
Nuestro servicio vivirá en un archivo llamado employees/services/employee.firestore.ts
. Este servicio extenderá el FirestoreService<T>
genérico creado anteriormente, y solo configuraremos la basePath
de la colección de Firestore:
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'; }
Luego, crearemos el archivo employees/states/employees-page.ts
. Esto servirá como el estado de la página de empleados:
import { Employee } from '../models/employee'; export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; }
El estado tendrá un valor de loading
que determina si mostrar un mensaje de carga en la página, los propios employees
y una variable formStatus
para manejar el estado del formulario (por ejemplo, Saving
o Saved
).
Necesitaremos un archivo en employees/services/employees-page.store.ts
. Aquí extenderemos el StoreService<T>
creado anteriormente. Estableceremos el nombre de la tienda, que se utilizará para identificarlo durante la depuración.
Este servicio inicializará y mantendrá el estado de la página de empleados. Tenga en cuenta que el constructor llama a super()
con el estado inicial de la página. En este caso, inicializaremos el estado con loading=true
y una matriz vacía de empleados.
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: [], }) } }
Ahora vamos a crear EmployeesService
para integrar EmployeeFirestore
y EmployeesPageStore
:
ng gs employees/services/Employees
Tenga en cuenta que estamos inyectando EmployeeFirestore
y EmployeesPageStore
en este servicio. Esto significa que EmployeesService
contendrá y coordinará las llamadas a Firestore y a la tienda para actualizar el estado. Esto nos ayudará a crear una sola API para que llamen los componentes.
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") }) } }
Echemos un vistazo a cómo funcionará el servicio.

En el constructor, nos suscribiremos a la colección de empleados de Firestore. Tan pronto como Firestore emita datos de la colección, actualizaremos la tienda, configurando loading=false
y employees
con la colección devuelta de Firestore. Dado que hemos inyectado EmployeeFirestore
, los objetos devueltos por Firestore se escriben en Employee
, lo que habilita más funciones de IntelliSense.
Esta suscripción estará activa mientras la aplicación esté activa, escuchando todos los cambios y actualizando la tienda cada vez que Firestore transmita datos.
this.firestore.collection$().pipe( tap(employees => { this.store.patch({ loading: false, employees, }, `employees collection subscription`) }) ).subscribe()
Las funciones employees$()
y loading$()
seleccionarán la parte del estado que queremos usar más adelante en el componente. employees$()
devolverá una matriz vacía cuando el estado se esté cargando. Esto nos permitirá mostrar mensajes adecuados en la vista.
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)) }
Bien, ahora tenemos todos los servicios listos y podemos construir nuestros componentes de vista. Pero antes de hacer eso, un repaso rápido podría ser útil...
Observables RxJs y la tubería async
Los observables permiten a los suscriptores recibir emisiones de datos como un flujo. Esto, en combinación con la tubería async
, puede ser muy poderoso.
La tubería async
se encarga de suscribirse a un Observable y actualizar la vista cuando se emiten nuevos datos. Más importante aún, cancela automáticamente la suscripción cuando se destruye el componente, protegiéndonos de pérdidas de memoria.
Puede leer más sobre la biblioteca Observables y RxJs en general en los documentos oficiales.
Creación de los componentes de la vista
En employees/components/employees-page/employees-page.component.html
, pondremos este código:
<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>
Del mismo modo, employees/components/employees-list/employees-list.component.html
tendrá esto, utilizando la técnica de canalización async
mencionada anteriormente:
<div *ngIf="loading$ | async"> Loading... </div> <div *ngIf="noResults$ | async"> No results </div> <div class="card bg-light mb-3" *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>
Pero en este caso también necesitaremos código TypeScript para el componente. El archivo employees/components/employees-list/employees-list.component.ts
necesitará esto:
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); } }
Entonces, yendo al navegador, lo que tendremos ahora es:
Y la consola tendrá la siguiente salida:
Mirando esto, podemos decir que Firestore transmitió la colección de employees
con valores vacíos, y la tienda employees-page
fue parcheada, configurando la loading
de true
a false
.
Bien, construyamos el formulario para agregar nuevos empleados a Firestore:
El Formulario de Empleados
En employees/components/employees-form/employees-form.component.html
agregaremos este código:
<form [formGroup]="form" (ngSubmit)="submit()"> <div class="form-group"> <label for="name">Name</label> <input type="string" class="form-control" 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" 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>
El código TypeScript correspondiente vivirá en 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() } }
El formulario llamará al método create()
de EmployeesService
. En este momento la página se ve así:
Echemos un vistazo a lo que sucede cuando agregamos un nuevo empleado.
Agregar un nuevo empleado
Después de agregar un nuevo empleado, veremos lo siguiente en la consola:
Estos son todos los eventos que se desencadenan al agregar un nuevo empleado. Miremos más de cerca.
Cuando llamemos a create()
, ejecutaremos el siguiente código, configurando loading=true
, formStatus='Saving...'
y la matriz de employees
para vaciar ( (1)
en la imagen de arriba).
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") })
A continuación, llamamos al servicio base de Firestore para crear el empleado, que registra (4)
. En la devolución de llamada de la promesa, configuramos formStatus='Saved!'
y registro (5)
. Finalmente, establecemos un tiempo de espera para que formStatus
vuelva a estar vacío, registrando (6)
.
Los eventos de registro (2)
y (3)
son los eventos activados por la suscripción de Firestore a la colección de empleados. Cuando se crea una instancia de EmployeesService
, nos suscribimos a la colección y recibimos la colección cada vez que se produce un cambio.
Esto establece un nuevo estado para la tienda con loading=false
configurando la matriz de employees
para los empleados que provienen de Firestore.
Si expandimos los grupos de registros, veremos datos detallados de cada evento y actualización de la tienda, con el valor anterior y el siguiente, lo cual es útil para la depuración.
Así es como se ve la página después de agregar un nuevo empleado:
Adición de un componente de resumen
Digamos que ahora queremos mostrar algunos datos de resumen en nuestra página. Digamos que queremos el número total de empleados, cuántos son conductores y cuántos son de Rosario.
Comenzaremos agregando las nuevas propiedades de estado al modelo de estado de página en employees/states/employees-page.ts
:
// ... export interface EmployeesPage { loading: boolean; employees: Employee[]; formStatus: string; totalEmployees: number; totalDrivers: number; totalRosarioEmployees: number; }
Y los inicializaremos en la tienda en employees/services/emplyees-page.store.ts
:
// ... constructor() { super({ loading: true, employees: [], totalDrivers: 0, totalEmployees: 0, totalRosarioEmployees: 0 }) } // ...
A continuación, calcularemos los valores de las nuevas propiedades y agregaremos sus respectivos selectores en 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)) } // ...
Ahora, vamos a crear el componente de resumen:
ng gc employees/components/EmployeesSummary
Pondremos esto en 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>
Y en 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$; } }
Luego agregaremos el componente a employees/employees-page/employees-page.component.html
:
// ... <div class="col-12 mb-3"> <h4> Employees </h4> <app-employees-summary></app-employees-summary> </div> // ...
El resultado es el siguiente:
En la consola tenemos:
El servicio de empleados calcula el total totalEmployees
, totalDrivers
y totalRosarioEmployees
en cada emisión y actualiza el estado.
El código completo de este tutorial está disponible en GitHub y también hay una demostración en vivo.
Administrar el estado de la aplicación angular usando observables... ¡Comprobado!
En este tutorial, cubrimos un enfoque simple para administrar el estado en aplicaciones Angular usando un back-end de Firebase.
Este enfoque encaja muy bien con las pautas de Angular sobre el uso de Observables. También facilita la depuración al proporcionar un seguimiento de todas las actualizaciones del estado de la aplicación.
El servicio de tienda genérica también se puede usar para administrar el estado de las aplicaciones que no usan las funciones de Firebase, ya sea para administrar solo los datos de la aplicación o los datos que provienen de otras API.
Pero antes de aplicar esto indiscriminadamente, una cosa a considerar es que EmployeesService
se suscribe a Firestore en el constructor y sigue escuchando mientras la aplicación está activa. Esto podría ser útil si usamos la lista de empleados en varias páginas de la aplicación, para evitar obtener datos de Firestore al navegar entre páginas.
Pero esta podría no ser la mejor opción en otros escenarios, como si solo necesita extraer los valores iniciales una vez y luego activar manualmente las recargas de datos de Firebase. La conclusión es que siempre es importante comprender los requisitos de su aplicación para elegir mejores métodos de implementación.