Observables, Subjects y Data Stores en entorno Angular

Picture of Ricardo Ahumada

Ricardo Ahumada

Tabla de contenidos

Un caso de uso recurrente de Observables es en el entorno de Angular, por ejemplo, para comunicar Servicios y Componentes. Este suele ser el caso cuando un servicio hace de Store y se comunica con una API Restful para actualizar el estado de los componentes de vista de una aplicación.

Por su parte, Angular usa Observables en su funcionalidad core. Por ejemplo, el Router de Angular tiene un método de eventos que crea un Observable de la URL. Cada vez que la URL cambia este Observable emite la nueva url (el mensaje emitido es en realidad más complejo).

Observables

Debemos recordar que los Observables tiene algunas cualidades:

  1. Los observables son perezosos (lazy)

Se puede pensar en los observables perezosos como newsletters . Para cada suscriptor, se crea un nuevo newsletter. Estas solo se envían las personas suscritas, y a nadie más.

  1. Los observables pueden tener múltiples valores a lo largo del tiempo

Si mantenemos abierta la suscripción al newsletter, recibiremos una nueva de vez en cuando. El remitente decide cuándo lo envía, todo lo que tenemos que hacer es esperar hasta que llegue directamente a nuestra bandeja de entrada.

Push vs pull

Un concepto clave para entender cuando se usan observables es que los observables hacen push. Hacer push y pull son dos formas que describen cómo un productor de datos se comunica con el consumidor de datos.

Pull

Al hacer pull (tirar), el consumidor de datos decide cuándo obtiene los datos del productor de datos. El productor no sabe cuándo se entregarán los datos al consumidor.

Las funciones de javascript se comportan así. La función es un Productor de datos, y el código que llama a la función la está consumiendo al “extraer” un solo valor de retorno de su llamada.

Push

Hacer push (empujar), funciona al revés. El productor de datos decide cuándo el consumidor obtiene la información.

Nosotros podemos crear nuestros propios Observables, pero para ello necesitamos un Subject

El Subject

Según indica la documentación de RxJS:

Un Subject es una especie de puente o proxy […] que actúa como observador y como observable. Debido a que es un Observer, puede suscribirse a uno o más Observables, y como es un Observable, puede pasar por los elementos que observa re-emitiéndolos, y también puede emitir nuevos elementos.

Por tanto un Subject es capaz de observar un evento y emitir un mensaje; a la vez de ser capaz de observado por otro elemento.

Para definir un Subject en Angular lo podemos hacer de la siguiente manera:

import { Injectable } from ‘@angular/core’;import { Subject } from “rxjs/Subject”;import { Model } from ‘./model; @Injectable()export class ModelService {public $models= new Subject<Model>();constructor() { }public fetchModels():{return $models;}} 

Usualmente a los observables se les prefija con $ por convención

Asimismo, de cara a un buen rendimiento es buena práctica crear una suscripción (objeto Subscription) cuando nos suscribimos a un Observable (en ngOnInit) para poder de-suscribirnos cuando el componente se destruya (en ngOnDestroy). De esta manera evitaremos memory leaks. Tal como indica la documentación de RxJS:

“[Subscription] representa un recurso desechable, como la ejecución de un Observable. Una Suscripción tiene un método importante, darse de baja, que no toma ningún argumento y solo elimina el recurso que posee la suscripción.

import { Component, OnInit, OnDestroy, Input } from ‘@angular/core’;import { Model } from ‘../model’;import { ModelService } from ‘../model.service’;import { Subscription } from “rxjs/Subscription”; @Component({selector: ‘app-model’,templateUrl: ‘./model.component.html’,styleUrls: [‘./model.component.css’]})export class ModelComponent implements OnInit, OnDestroy { @Input() model: Model;private _modelSubscription: Subscription; constructor(private modelService: ModelService) { } ngOnInit() {this.modelSubscription = this.modelService.$visible.subscribe((model: Model) => {this.model.isVisible = model.title === this.model.title;})} ngOnDestroy() {if (this.modelSubscription) {this.i_temSubscription.unsubscribe();}} }

Consumir Observables desde un componente Angular

En Angular podemos suscribirnos a un Observables de dos maneras:

En plantilla

Nos suscribimos a un observable en la plantilla del componente usando el pipe async. El beneficio de esta aproximación es que Angular gestiona la suscripción durante el ciclo de vida del componente. Angular se suscribirá y cancelará la suscripción automáticamente. Para ello es necesario importar el “CommonModule” en el módulo del proyecto para tener acceso al pipe aync.

En la clase:

public models$: Observable<Model[]> constructor(private _modelService: ModelService ) {} public ngOnInit() {this.models$ = this._modelService.fetchModels()} 

En la plantilla

<ul class=”model__list” *ngIf=”(models$ | async).length”><li class=”model” *ngFor=”let model of models$ | async”>{{ model.attribute1 }} – {{ model.attribute2 }}</li></ul> 

En la clase

Nos suscribimos al observable usando el método subscribe(). Esto puede ser útil si primero se desea hacer algo con los datos antes de mostrarlos. La desventaja es que usted debe gestionar la suscripción (y de-suscripción) por uno mismo.

public models: IModel[] constructor(private _modelService: ModelService ) {} public ngOnInit() {this._modelService.fetchUsers().subscribe((models: IModel[]) => { // procesado de datos de modelos.// ….this.models = modelss})}

De manera general, se recomienda el modo en plantilla, ya que es la manera más sencilla y evita gestionar manualmente la suscripción.

Los 4 tipos de observables

RxJS nos provee con 4 tipos distintos de Subject que nos permiten crear un Observable:

  • Subject
  • BehaviorSubject
  • ReplaySubject
  • AsyncSubject

La principal diferencia entre ellos es la manera en que responden a una subscripción a través de subscribe().

Subject

Cuando nos suscribimos a un Suject, se obtienen todos los eventos que este Sujeto emite después de haberse suscrito como se muestra en la figura.

Es decir, con Subject, el observador nunca será notificado del evento 1 y 2

BehaviorSubject

Un BehaviorSubject se comporta como un Subject, excepto que el Observer también recibe el último evento que ocurrió antes de la suscripción. A continuación, recibe todos los eventos que ocurren después de la suscripción, al igual que para un Subject regular.

En este caso, el obervador será notificado del evento 2 cuando se suscriba, pero no del evento 1.

ReplaySubject

Con un ReplaySubject, el Observer recibe todos los eventos pasados cuando se suscribe. A continuación, recibe todos los eventos que ocurren después de la suscripción, al igual que para un sujeto regular.

El observador conocerá todos los eventos previos, es decir 1 y 2.

AsyncSubject

Tiene un comportamiento peculiar. AsyncSubject esperará a completarse para emitir el último evento y luego el evento complete.

El  observador solo será notificado del evento 4 (y complete).

Data stores en Angular: Services, Componentes, Observables y Flux

Como hemos visto, los observables son una manera flexible y eficiente para comunicar servicios con componentes.

En una aplicación real, tendremos que muchos componentes necesitarán tener acceso a datos comunes de la aplicación. Pensemos por ejemplo en una aplicación sencilla de proyectos y tareas. Podríamos tener vistas de proyectos (lista, detalle, crear/editar) y tareas (lista, detalle, crear/editar). Ya que existe dependencia entre los dos modelos (las tareas pertenecen a un proyecto, los proyectos tienen tareas), necesitaremos que los componentes tengan acceso a esos datos comunes para mostrarlos en la interface. A este conjunto de datos (valores de los modelos) que gestionamos en una aplicación se llama estado de la aplicación.

Uno de los problemas más interesantes que se abordan durante el desarrollo de una aplicación es administrar el estado. Los nuevos paradigmas han cambiado la forma en que las aplicaciones web tratan y manipulan su estado, desviándose de los mecanismos de enlace de datos bidireccionales (como se hacía en AngularJS) a un flujo unidireccional más funcional, tal como plantea el patrón Flux con React y Redux.

Para aquellos que quieran trabajar sin dependencias de terceros, Angular ofrece un par de herramientas que abordan la mayoría de las preocupaciones de administración de estado:

  • @Input y @Output: pasan el estado a través de una cadena de componentes anidados mediante atributos y emisores de eventos.
<td><boton-borrar [tarea]=”unaTarea” (borrarClicked)=”borrar($event)”></boton-borrar></td>
  • Providers / Servicios: gestionan los datos mediante inyección de dependencia
@Injectable()export class TareasService {private _tareas:Tarea[];…..}
 

Los servicios como data store

Cuando se trata de una aplicación un poco grande, queremos evitar los servicios que contienen demasiada lógica de negocio y cubren una gran cantidad de datos mutables. Por tanto podemos usar los servicio como un “data store” de manera sencilla usando el estado de los mismos y observables.

La idea detrás es triple:

  1. Aislar una parte de los datos en un servicio para que pueda ser compartida a lo largo y ancho de una aplicación
  2. Moderar las mutaciones a estos datos para que los cambios puedan rastrearse fácilmente
  3. Limitar el grado en que se puede cambiar el estado para evitar efectos secundarios no deseados.

Un servicio tipo tendría la siguente forma:

import { Injectable } from ‘@angular/core’;import { Observable } from ‘rxjs/Rx’;import {  HttpClient, HttpHeaders, HttpParams, HttpResponse } from ‘@angular/common/http’; import { Tarea } from ‘../models/tarea’; @Injectable()export class TareasService {private _tareas:Tarea[];private _api=”url_api”;private _tareasObs:Observable<Tarea[]>; constructor(private _http: HttpClient){}  getTareas(){if(this._tareas){return Observable.of(this._tareas);}else if(this._tareasObs){return this._tareasObs;}else{this._tareasObs= this._http.get<Tarea[]>(this._api).map((tareasfromapi: Tarea[]) => {this._tareas=tareasfromapi;return this._tareas;}).do((tareas: Tarea[]) => {console.log(‘getTareas…’,this._tareas );}).catch(this.handleError); return this._tareasObs;} } borrarTareaById(tid: number): void {//actualiza la store después de comunicar con la API}  }

Asimismo, un componente que consumiera dicho servicio tendría la siguiente forma:

@Component({selector: ‘app-tareas’,templateUrl: ‘./tareas.component.html’,styleUrls: [‘./tareas.component.css’]})export class TareasComponent implements OnInit {textoafiltrar=”;$tareas:Tarea[]; constructor(private _tareasService:TareasService) { } ngOnInit() {this.$tareas=this._tareasService.getTareas(); //usaremos async} borrar(tid: number): void {this._tareasService.borrarTareaById(tid);} }

Aunque este componente es bastante simple, los principios básicos de un data store aún se aplican. Dentro del componente, pasamos nuestro store de datos a través de la DI (Dependency Inyectio) de Angular con la línea: constructor (private appState: AppState) {} para que podamos acceder al estado de la aplicación en el componente.

Dentro de la función de ciclo de vida ngOnInit (), nos suscribimos al estado de la aplicación (en este caso, una lista de tareas) que nos proporcionará un nuevo estado cada vez que se cambie la store mediante un observable fruto de  un BehaviourSubject.

Para modificar el estado interactuamos con la interface (por ejemplo borrando una tarea) e invocamos al servicio para mutar los datos.

Gracias a la suscripción, la store contiene los datos actualizados de la aplicación. Cada vez que se cambian los datos, todos los suscriptores a la store (por ejemplo, el componte de proyectos) se actualizarán automáticamente. Con la ayuda del BehaviorSubject privado, cualquier componente que quiera cambiar el estado en la store tiene que hacerlo a través de la misma, asegurando que nadie pueda manipular los datos de una manera que rompa la aplicación.

En el componente podemos tener componentes contendores hijos que puedan recibir la información mediante @input y comunicar interacciones mediante emisión de eventos con @output.

<td><boton-borrar [tid]=”unaTarea.tid” (borrarClicked)=”borrar($event)”></boton-borrar></td>

De esta manera tendremos una aplicación sencilla del patrón flux en nuestra aplicación.

Reflexiones finales

Tal como hemos visto, crear observables en Angular es relativamente sencillo, simplemente debemos definir la fuente a observar y usar algunos de los subject que proporciona RxJS o los objetos de Angular que ya proporcionan observables.

Podemos elegir entre distintos tipos de observables en función del comportamiento que queremos implementar en el observador.

Pero tenemos que tener cuidado de evitar memory leaks, gestionando la suscripción y de-suscripción a los observables.

Podemos usar los servicios en combinación con observables para implementar arquitecturas más interesantes u útiles como una aplicación basada en stor y así implementar un variante sencilla del patrón Flux.

Forma parte de la comunidad #AlwaysLearning

¡Síguenos la pista!

Sobre el autor

Picture of Ricardo Ahumada

Ricardo Ahumada

Insights relacionados

Formación

  • Sensibilización en la importancia de las e-Competences
  • Capacitación Técnica y en Gestión de la Tecnología
  • Formación a medida
  • Adaptación de contenidos propios a formación presencial y online

CONTÁCTANOS

Netmind España
Barcelona +34 933 041 720
Madrid +34 914 427 703

Nos puedes encontrar de:
Lunes – Viernes, 9:00-18:00 (GMT+1)

¡Te ayudamos!
info@netmind.net

¿Dudas sobre servicios/formaciones?
comercial@netmind.net

Buscar

Solicitar Información

Request Information