RxJS en Angular: El drama de las suscripciones
Hace un tiempo que no escribía una nueva entrada en el blog, no por falta de ideas, sino por falta de tiempo. Arranqué a trabajar en proyectos con Angular y Firebase. Estos proyectos me enseñaron mil cosas sobre las suscripciones, los observables y métodos asincrónicos, que hasta hoy venia ignorando.
Hasta este tiempo estuve utilizando llamados asincrónicos al backend sin saber exactamente que pasaba, el porque de la utilización de observables y, realmente, para qué servían. A veces pasa, en este mundo, que utilizamos las cosas sin saber y solamente si funcionan ya está, las dejamos ahí, pero todo tiene un porque y como trabajan.
Normalmente en Angular, cuando hacemos peticiones a nuestras APIs que están en el backend, usamos la librería que nos provee Angular llamada: HttpClient. Esta nos maneja todo el tema de llamado asincrónico (a veces usan fetch, en su lugar).
HttpClient nos retorna un observable al llamarlo y nosotros nos subscribimos a este para obtener valores, pero realmente, ¿Qué es un Observable? ¿Que es una suscripción? En un post anterior mio, hago una minima referencia a esto.
¿Qué es un observable?
Tal como dice su nombre, es algo que observa, se mantiene a la espera de que algo cambia, como si espiara y cuando haya algún cambio nos avisa. Es un método asincrónico que viene de la librería RxJS (tranquilos, mas adelante en el post les explico que es). Cuando tenemos un backend, ya sea en APIs, Sockets o Firebase, nuestro observable va a estar buscando respuesta en estos y cuando haya un cambió nos avisará, ¿Cuál es el problema? En el caso de las APIs, la respuesta va a ser una sola y la conexión se cerrará, pero nuestro observable seguirá viviendo mientras estemos suscritos y esto podría traer problemas de performance en ambos lados (Front y Back).
¿Qué es una suscripción ó subscribe?
Acá viene lo interesante, si nuestro observable es el encargado de "Observar" una respuesta, la suscripción es la encargada de recibir esta respuesta de parte de él y luego, con esto, realizar una acción. El problema viene acá, si nosotros no decimos que queremos dejar de escuchar a nuestro observable, vamos a seguir suscritos hasta que la aplicación muera y esto es un grave problema. Sea cual sea el backend que estés utilizando, todo tiene su costo, toda petición que dejemos abierta "escuchando" o "esperando" respuesta nos consumirá tanto a nuestro frontend como al backend y esto traerá problemas en performance y presupuesto (entre mayor sea la cantidad de request, mas caro es el backend).
¿Cómo puedo manejar correctamente una suscripción en Angular?
Existen varias formas de manejar una suscripción la mayoria vamos a tener que requerir de RxJS, pero ¿Qué es esto?
¿Qué es RxJS?
Como mencione antes, en un post mio hago referencia a esta librería (Carrito Reactivo con Angular y RxJS). Pero básicamente, RxJS es una librería de Javascript orientada al manejo de Observables. Detras de esto hay mucho laburo y mucho manejo de operaciones asincrónicas. La documentación es muy detallada y MUY buena.
Ahora que tenemos una introducción, vamos a ver como manejar una suscripcion.
Supongamos que tenemos un servicio que tiene el siguiente método para obtener las categorías de vehículos
constructor(private _httpClient: HttpClient) { }
public getCategorias(): Observable<any> {
return this._httpClient.get('URLAPI'); //Nos retorna un observable
}
Básicamente, el httpClient.get('URLAPI') nos retorna un observable a nuestra API.
Ahora, ¿Cómo nos suscribimos?. Bueno, como dije antes, hay varias formas
Usando el pipe async para suscribirse
Vamos a utilizar el pipe async para suscribirnos, este se encarga automáticamente cuando se destruye el componente de desuscribirse a nuestro observable y así, evitar sobrecargas innecesarias.
¿Como funciona?
Para usarlo debemos guardarlo dentro de una variable del tipo "Observable".
Supongamos que tenemos en nuestro component.ts un llamado a nuestro servicio visto mas arriba.
public obsCatService:Observable<any>
constructor(public _categoriaService: CategoriaService) {
obsCatService = this._categoriaService.getCategorias()
}
Cree una variable llamada obsCatService y allí guarde la llamada al observable. Solo queda usar el pipe
<option *ngFor="let c of obsCatService | async" [value]="c">
{{c.name}}
</option>
Tengo un dropdown con un option, allí uso el ngfor con el pipe async y luego uso las propiedades del objeto que me devuelve mi observable (en mi caso me devuelve un name que es el nombre de la categoría), también guardo el objeto categoría en el value de cada uno, así puedo obtener referencia rápidamente de cual seleccioné.
Este método es el mas facil de usar y además tiene la ventaja (si sos olvidadizo como yo) de automáticamente desuscribirse cuando se destruye el componente.
Vamos con otra manera.
Desuscribirse manualmente de nuestro observable
Esta forma es quizás mas útil si tienen que hacer operaciones con la devolución de su observable (por ejemplo, filtrar su contenido). La diferencia es que, ahora haremos todo desde el ts y en lugar de una variable del tipo observable, deberá ser del tipos subscription.
Vamos a ver un ejemplo:
export class DialogProductoComponent implements OnInit, OnDestroy {
private subs: Subscription;
constructor(public _categoriaService: CategoriaService) {
subs = this._categoriaService.getCategorias().subscribe(listCat=>{
//HAGAMOS LO QUE NECESITEMOS ACÁ
})
}
ngOnDestroy(){
this.subs.unsubscribe();
}
}
Claramente vemos la diferencia, usamos el ngDestroy (evento cuando se destruye el componente) para desuscribirnos de nuestro observable.
Manejando suscripciones con funciones RxJS
Hay algunas funciones de RxJS que nos permitirían hacer esto.
take(n)
La función take(n) nos permite tomar n veces el resultado devuelto por nuestro observable, si ponemos take(1) obtendrá el primer valor que entregue. La ventaja de esto es que, automáticamente, se desuscribirá al obtenerlo.
takeUntil(notifier)
Obtendremos el valor de nuestro observable hasta que otro diga lo contrario, si lo sé, para que c*** hacemos esto? Bueno, descubrirán que es muy útil si lo combinamos con Subjects o BehaviourSubjects. Acá esta la documentación
first(condition)
Es una especie de "concatenacion" entre take y takeUntil. Nos permite obtener el observable hasta que se cumpla cierta condición (la decidimos nosotros) en el primer valor que retorna el mismo. Documentación
shareReplay()
Esta función no la conocia hasta que vi un video de Jorge Cano explicando como funciona. Lo que hace es evitar los llamados innecesarios al backend, ya que comparte la entrega de datos en donde lo llamemos. Imaginense que es como una especie de "storage" que almacena el valor que le digamos y compara con el próximo, si este no cambió no lo deja pasar a nuestra suscripción, pero si es el caso contrario lo deja pasar y actualiza este "storage".
El fin del fin
Hasta acá llegamos, espero que les sirva todas estas lecciones, es muy sabio manejar correctamente las suscripciones y observables ya que nos ahorra muchos dolores de cabeza.