Múltiplas requisições com forkJoin - RxJS - Angular
Veja como paralelizar as requisições e saber quando todas foram finalizadas com o operador forkJoin do RxJS em um projeto Angular.
Algumas aplicações precisam consultar várias APIs antes de mostrar a primeira tela para o usuário. Durante este carregamento é de praxe mostrar uma mensagem de "loading" para o usuário saber que sua aplicação está sendo carregada.
O que complica as coisas é quando temos várias requisições acontecendo ao mesmo tempo e precisamos saber se todas estas requisições foram finalizadas para que o "loading" possa ser ocultado.
Para entender este cenário considere o código abaixo onde o componente realiza 2 consultas à APIs e depois oculta o "loading":
Analisando o código podemos ver que inicialmente o usuário irá visualizar a mensagem Carregando, por favor aguarde...
já que a variável carregando
tem o valor inicial false
. Enquanto isto a aplicação irá consultar as APIs e quando tudo isso acabar, o "loading" será ocultado dando lugar aos dados na tela.
Deixei alguns pontos de console no código acima para facilitar a explicação e o entendimento da ordem de execução:
- Chamamos a função
carregarDadosService1()
console.log
logo no início da funçãocarregarDadosService1()
- Depois de alguns milisegundos a primeira service nos entrega os dados e então a
carregarDadosService2()
é chamada console.log
na primeira linha dentro da funçãocarregarDadosService2()
- Depois de alguns milissegundos a segunda service nos entrega os dados e então ocultamos a mensagem de "loading..." através da variável
carregando = false
Observação: deixei algumas variáveis para calcularmos o tempo para finalizar as 2 requisições. Os resultados foram 753ms e 645ms para terminar o carregamento das 2 requisições. Mais a frente vamos compará-lo com o uso do forkJoin
.
O ponto negativo que podemos destacar no exemplo acima é que as requisições são sequenciais, ou seja, só iniciamos a segunda requisição quando a primeira for finalizada.
Se tentarmos paralelizar as duas requisições, teremos um aumento na complexidade do controle da variável carregando
porque que teremos que acompanhar o andamento das duas requisições e somente quando as duas requisições forem finalizadas atualizaremos a variável carregando
para false
.
Aguardando o resultado das requisições com forkJoin
Para resolver este problema podemos utilizar o forkJoin
do RxJS. Ele acompanha o andamento das requisições (observables) passadas como parâmetro e emite um sinal quando todas as requisições forem finalizadas. Bem mais simples, não acha?
Refatorando um pouco, temos:
import { Component, OnInit } from '@angular/core';
import { Teste1Service } from './teste1.service';
import { Teste2Service } from './teste2.service';
import { forkJoin } from 'rxjs';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
inicio: Date;
termino: Date;
carregando: boolean = true;
dadosService1: any;
dadosService2: any;
constructor(private teste1Service: Teste1Service,
private teste2Service: Teste2Service) { }
ngOnInit() {
this.inicio = new Date();
forkJoin({
dados1: this.teste1Service.carregar(),
dados2: this.teste2Service.carregar(),
}).subscribe(resultado => {
this.dadosService1 = resultado.dados1;
this.dadosService2 = resultado.dados2;
this.carregando = false;
this.termino = new Date();
const tempoMS = this.termino.getTime() - this.inicio.getTime();
console.log('Tempo de execução (ms): ', tempoMS);
});
}
}
forkJoin
will wait for all passed observables to complete and then it will emit an array or an object with last values from corresponding observables.
fonte: https://rxjs-dev.firebaseapp.com/api/index/function/forkJoin
O resultado final é exatamente o mesmo, porém podemos destacar que a execução das consultas em paralelo é bem mais performática. Enquanto na consulta sequêncial obtivemos algo em torno de 700ms, na execução em paralelo obtivemos aproximadamente 270ms para concluir as requisições. Além disto, deixamos o código muito mais simples.
Tratando erros
Não podemos deixar de falar sobre o tratamento de erros, a final de contas, eventualmente uma das requisições pode falhar.
Quando usamos o forkJoin
, caso alguma das requisições retorne erro, ele emitirá o erro imediatamente e efetuará o unsubscribe nos outros observables. Veja no exemplo abaixo:
import { Component, OnInit } from '@angular/core';
import { Teste1Service } from './teste1.service';
import { Teste2Service } from './teste2.service';
import { forkJoin } from 'rxjs';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
inicio: Date;
termino: Date;
carregando: boolean = true;
dadosService1: any;
dadosService2: any;
constructor(private teste1Service: Teste1Service,
private teste2Service: Teste2Service) { }
ngOnInit() {
this.inicio = new Date();
forkJoin({
dados1: this.teste1Service.carregar(),
dados2: this.teste2Service.carregar(),
dados3: this.teste2Service.carregarErro(), // << irá gerar erro
}).subscribe(resultado => {
this.dadosService1 = resultado.dados1;
this.dadosService2 = resultado.dados2;
this.carregando = false;
this.termino = new Date();
const tempoMS = this.termino.getTime() - this.inicio.getTime();
console.log('Tempo de execução (ms): ', tempoMS);
}, erro => {
// Se umas das 3 requisições der erro, este
// trecho será executado
console.log('Ocorreu um erro: ', erro);
this.carregando = false;
});
}
}
Na imagem abaixo deixei marcado o ponto de console que mostra o erro e na variável erro
conseguimos acessar o body
da resposta através do erro.error
:
Considerações
Veja que utilizando o forkJoin
as coisas ficaram bem mais simples. Podemos ter várias requisições e ainda assim saber exatamente quando todas forem concluídas.
Se você já trabalhou com Promises do JavaScript, com certeza vai lembrar do Promise.all
que tem o mesmo comportamento.
Abaixo há o link com o código fonte completo:
Links interessantes: