Usando concatMap, mergeMap y forkJoin en Angular para peticiones HTTP

Empezaré por dar contexto a la situación, actualmente he estado realizando múltiples peticiones para traer registros basados en fechas desde X fecha hasta Y fecha. Inicialmente por mi poco conocimiento de Angular hacía un ciclo un poco raro pero empezaba a cargar todo como loco por lo que mi primer acercamiento estuvo erróneo, así que investigando y aprendiendo conocí forkJoin que será el primer método que veremos.

forkJoin técnicamente lo que hace es generar peticiones en paralelo y esperar hasta que terminen todas para mostrar un resultado.

Primero les dejo el enlace a la documentación orignial: http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html

forkJoin lo que hace es recibir un arreglo de Observables como un argumento así se mapean las peticiones y retorna los resultados como una serie de arrays en la respuesta. Es decir, si por ejemplo tenemos un JSON que retorna como { book_id: 1, author: 15, name: 'Hello world' } entonces dentro de nuestra respuesta tendremos múltiples de estos por lo que necesitaremos hacer un ciclo para estar pasando entre cada respuesta.

Vamos a ver una función que nos servirá como base:

public getSomethingFromAnAPI(id: number): any {
 return this.http.get(
  'https://jsonplaceholder.typicode.com/posts/' + id
 );
}

Aquí veremos dos cosas:

  1. Esta es la versión más simple de una petición GET a un API.
  2. El API que estamos usando retorna datos dummy y pasaremos hasta 10 IDs para este ejemplo.

Entendiendo esto, entonces vamos a generar un array de 10 IDs que vamos a tratar de traer con forkJoin . Como mencioné previamente, requerimos que estos IDs sean provenientes de un Observable y tal cual nos permite HttpClient en Angular traer como en el código anterior. Entonces escribimos el siguiente código:

let ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let arrayOfData = [];
for(let i = 0; i < ids.length; i++) {
 arrayOfData.push(this.getSomethingFromAnAPI(ids[i]));
}

Lo que hacemos es como mencioné tener 10 IDs, después en un ciclo para no tener que escribir a mano dentro del arrayOfData lo metemos y así tenemos listo nuestro arreglo de Observables para pasar a forkJoin .Entendiendo esto, pasaremos el código anterior para poder implementarlo en conjunto con forkJoin de la siguiente manera:

forkJoin(arrayOfData).subscribe(response => {
  for(let item in Object.keys(response)) {
    this.data.push({
      body: response[item].body, 
      title: response[item].title, 
      id: response[item].id
    });
  }
}, error => {
  console.error(error);
});

Y hasta aquí entonces, ¿qué hace forkJoin ?. Primero analicemos el código, lo primero es que, una vez que responda, lo pasamos a una variable de información que usaremos para imprimir de forma simple:

<ul>
 <li *ngFor="let item of data">
  <h4>#{{item.id}} - {{item.title}}</h4>
  <p>{{ item.body }}</p>
 </li>
</ul>

Y si no responde correctamente, caerá sobre error y pasaremos esto a consola para saber qué pasó a la hora de llamar la información.

Si entonces, forkJoin respondió correctamente a todas nuestras peticiones, la consola en el apartado de network la veremos de la siguiente manera:

En este caso, no se alcanza a ver correctamente en nuestro ejemplo pero, lo veremos con un muy buen gif que nos ilustra mejor esta network propiedad de Tomas Trajan en otro proyecto, Hakafaka.

Pero, ¿qué demonios significa esto?. Bueno, técnicamente lo que pasa con forkJoin es que primero manda pedir al recurso saber si está disponible en cada uno, si retorna un estado 200/Ok significa que lo está, y entonces empiezan las peticiones reales.

Una vez que termina TODAS las peticiones, forkJoin , responde…es entonces que, aquí hay dos problemas:

  1. Necesitamos esperar a que responda en su totalidad todas las peticiones para poder manipular la información.
  2. Necesitamos que todas las respuestas sean correctas, si alguna falla, toda la cadena de peticiones falla.

Ahora, como menciono, imaginen que cada una trae 10,000 registros y que esos 10,000 registros son 30 días los que buscan, serían 300,000 registros, pensemos que 29 días los logra pero el día 30 no puede con ello por saturación o X cosa, y truena, todas las demás peticiones por lo tanto habrán tronado y se irá todo a la basura.

Además, no es amigable al usuario, ya que, deberá esperar a que todas terminen para ir viendo información.

Entonces, ¿qué pasa con todo esto?, en lo personal y después de haber utilizado forkJoin creo que el mejor uso para esto es cuando tenemos que cargar poca data pero de jalón, un caso podría ser que, necesitemos mostrar las últimas 10 publicaciones, no necesitamos ir imprimiendo 1 por 1, por lo que podemos imprimir las 10 de jalón.

¿Y qué pasa cuando sí debo imprimir 1 por 1 y que me importe que si falla una petición no se pierda la cadena de peticiones?. Es ahí cuando entra concatMap

Vamos a modificar el código:

public getSomethingFromAnAPI(ids: number[]): any {
  return from(ids).pipe(
     concatMap(id => <Observable<any>> this.http.get('https://jsonplaceholder.typicode.com/posts/' + id))
     ); 
}

Con esta forma vamos a permitir que retorne un Observable que de igual forma es requerido para poder manejar la data en el subscribe, pero ahora incluimos el concatMap que es parte de RxJS en el método from que requiere nuestros IDs en formato de array.

let ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
this.getSomethingFromAnAPI(ids).subscribe(response => {
 this.data.push({
  body: response.body, 
  title: response.title, 
  id: response.id
 });
}, error => {
 console.error(error);
});

Como podemos ver es más sencillo en este estilo el código inicial (no la petición en sí), ya que no requerimos un ciclo para cuando termina de responder.

¿Qué pasa con concatMap además?, las peticiones se hacen lineales, es decir, una tras otra, termina la petición 1, sigue con la 2, termina la 2, sigue con la 3…y así sucesivamente hasta terminar.

Una vez más usaré un Gif de Tomas para ilustrar cómo se vería de mejor forma.

Lo bueno en concatMap es que si una petición termina y falla, la siguiente no se ve afectada, por lo que retomando el ejemplo de los 10,000 registros, si falla el 30, los otros 29 sí se muestran y únicamente el 30 fallará.

Adicionalmente, en concatMap las peticiones se realizan en orden, lo cual podría ser una desventaja también, ya que, si por ejemplo la petición 1 tiene 500 registros, la 2 tiene 10,000, la 3 tiene 100, entonces la 3 en teoría sería la más rápida, pero deberá esperar por las demás a que terminen.

¿Existe una solución a esto?, sí mergeMap es una función que lo permite. La diferencia con concatMap es que esta no tira las peticiones 1 por 1 esperando termine la anterior si no que empieza a tirarlas directo y una vez que responde una puedes usar esa respuesta y al igual que concatMap no interfiere con las demás, es decir, falla la 15 de los 30 días y no se ven afectados los otros 29.

Usemos otro gif de Tomas para explicar:

Como se ve todas las peticiones salen y no esperan a que la anterior termine, esta en teoría es la mejor opción pero, ¿cuándo no nos conviene utilizarla?.

Bueno, el inconveniente con este caso es que así como van respondiendo las peticiones es que vamos jalando la información y es ahí donde está el problema. Si la petición al día 3 como dije tiene 100 registros, va a responder quizá antes que la petición 1, por lo que se mostraría primero el día 3.

En realidad podemos hacer un filtrado a nivel del pipe es decir dentro de la petición y una vez que termina para hacer un reacomodo, pero, cuando estamos pensando en estos miles de registros, la máquina va a estar tardando en realizar el reacomodo de hasta 300,000 registros por lo que quizá no sea el mejor acercamiento en nuestro ejemplo.

Ahora, te preguntarás, ¿cómo se utiliza?, en nuestro ejemplo de código solo hace falta, literalmente, cambiar la palabra concatMap por mergeMap .

Si utilizas en nuestro código ejemplo mergeMap verás entonces algo así:

Que es básicamente lo que menciono, el error de que no va en orden y depende a la respuesta.

Te dejo un StackBlitz con los 3 fragmentos de código para los tres tipos y puedas usarlos:

https://stackblitz.com/edit/angular-bojdob

Si te gustó el tutorial deja tu “Clap”, sígueme para más y si tienes dudas, déjamelas en los comentarios. ¡Hasta la próxima!

¿Cuál es tu reacción?
+1
0
+1
0
+1
0
+1
0
+1
0
Total
0
Shares
Publicación anterior

Cómo comunicar dos componentes sin relación en Angular

Siguiente Publicación

Usando APP_INITIALIZER en Angular

Publicaciones Relacionadas