Como cancelar requisições HTTP anteriores com switchMap no Angular
Em uma pesquisa que consulta a API a cada alteração do usuário, várias requisições podem ficar concorrendo entre si. Com switchMap, a inscrição da requisição anterior é cancelada quando chega um novo valor, deixando apenas a pesquisa mais recente ativa.
O problema das requisições concorrentes
Imagine uma busca por nome de aluno. O usuário digita:
d
da
dan
dani
daniel
Se cada valor disparar uma chamada HTTP imediatamente, podemos ter cinco requisições em andamento. A ordem das respostas não é garantida.
A busca por dan pode demorar mais que a busca por daniel. Se ela terminar por último, o componente pode substituir um resultado atual por outro antigo.
A solução com switchMap
Em RxJS, switchMap troca a Observable interna sempre que a origem emite um novo valor. Ao fazer essa troca, ele cancela a inscrição da Observable anterior.
Com o HttpClient do Angular, cancelar essa inscrição também interrompe a requisição HTTP em andamento quando o backend utilizado pelo cliente permite o abort.
this.pesquisa.valueChanges
.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap(termo =>
this.service.buscar(termo)
)
)
.subscribe(resultado => {
this.itens = resultado;
});
O fluxo fica assim:
- o usuário altera o campo;
debounceTime(400)aguarda 400 ms sem novas alterações;distinctUntilChanged()evita repetir a mesma pesquisa consecutivamente;switchMap()cancela a inscrição da busca anterior;- somente a requisição mais recente produz resultado para esse fluxo.
Exemplo completo com FormControl
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import {
Subject,
catchError,
debounceTime,
distinctUntilChanged,
of,
switchMap,
takeUntil,
tap
} from 'rxjs';
@Component({
selector: 'app-pesquisa-aluno',
templateUrl: './pesquisa-aluno.component.html'
})
export class PesquisaAlunoComponent implements OnInit, OnDestroy {
pesquisa = new FormControl('', { nonNullable: true });
alunos: Aluno[] = [];
carregando = false;
erro = '';
private readonly destroy$ = new Subject<void>();
constructor(
private readonly alunoService: AlunoService
) {}
ngOnInit(): void {
this.pesquisa.valueChanges
.pipe(
debounceTime(400),
distinctUntilChanged(),
tap(() => {
this.carregando = true;
this.erro = '';
}),
switchMap(termo => {
const valor = termo.trim();
if (valor.length < 3) {
return of([] as Aluno[]);
}
return this.alunoService.buscar(valor).pipe(
catchError(() => {
this.erro = 'Não foi possível realizar a pesquisa.';
return of([] as Aluno[]);
})
);
}),
tap(() => {
this.carregando = false;
}),
takeUntil(this.destroy$)
)
.subscribe(alunos => {
this.alunos = alunos;
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Service com HttpClient
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AlunoService {
private readonly url = '/api/alunos';
constructor(
private readonly http: HttpClient
) {}
buscar(termo: string): Observable<Aluno[]> {
const params = new HttpParams()
.set('q', termo);
return this.http.get<Aluno[]>(
this.url,
{ params }
);
}
}
Não faça subscribe() dentro do service para essa situação. Retorne a Observable ao componente para que o switchMap controle a inscrição.
Não consulte a API com um termo vazio
switchMap(termo => {
const valor = termo.trim();
if (!valor) {
return of([]);
}
return this.alunoService.buscar(valor);
})
Também é comum exigir dois ou três caracteres antes de iniciar a busca. Isso reduz chamadas pouco úteis ao backend.
Loading: um detalhe importante
O loading deve representar a requisição que interessa ao usuário naquele momento. Em fluxos mais complexos, evite espalhar vários subscribe() e booleans independentes.
No exemplo, o estado é ligado antes do switchMap e desligado depois que a Observable mais recente produz resultado.
Onde colocar o catchError?
Em pesquisas contínuas, normalmente é melhor tratar o erro dentro da Observable retornada pelo switchMap:
switchMap(termo =>
this.alunoService.buscar(termo).pipe(
catchError(() => of([]))
)
)
Assim, um erro de uma chamada não encerra o fluxo de valueChanges. O usuário pode continuar digitando e fazer novas pesquisas.
switchMap, mergeMap, concatMap ou exhaustMap?
switchMap
// troca para a mais recente; a anterior deixa de participar do fluxo
mergeMap
// mantém operações em paralelo
concatMap
// executa em sequência, respeitando a fila
exhaustMap
// ignora novos valores enquanto a atual não termina
Para pesquisa digitada pelo usuário, switchMap costuma ser o comportamento desejado porque o resultado anterior perde utilidade quando existe um termo mais recente.
Quando mergeMap faz mais sentido?
Quando as operações são independentes e todas precisam ser concluídas.
Quando concatMap faz mais sentido?
Quando todas as operações devem acontecer e a ordem importa.
Quando exhaustMap faz mais sentido?
Quando novas ações devem ser ignoradas enquanto a operação atual ainda está em andamento, como em alguns fluxos de envio de formulário.
Quando não usar switchMap
switchMap quando descartar a operação anterior puder significar perder uma ação necessária.Uma pesquisa pode ser descartada porque só a mais recente importa. Já operações de gravação devem ser avaliadas com cuidado.
- salvar pedidos;
- registrar pagamentos;
- incluir lançamentos;
- enviar comandos que precisam ser processados;
- processos em que todas as solicitações precisam chegar ao backend.
E o takeUntil?
O switchMap resolve a troca das requisições internas, mas o fluxo principal de valueChanges continua inscrito enquanto o componente existir.
private readonly destroy$ = new Subject<void>();
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
e no pipeline:
takeUntil(this.destroy$)
Em projetos Angular mais novos, também existem mecanismos integrados ao framework para finalizar inscrições no ciclo de destruição. Use a alternativa compatível com a versão do seu projeto.
Fluxo recomendado para busca
valueChanges
→ debounceTime
→ distinctUntilChanged
→ normalizar/verificar termo
→ switchMap
→ HttpClient
→ catchError
→ atualizar tela
Combine com debounce
Se você ainda não configurou o atraso entre digitação e pesquisa, veja também Como aplicar debounce em pesquisa no Angular.
debounceTimereduz a quantidade de requisições;switchMapimpede que a pesquisa anterior continue competindo com a nova.
Checklist rápido
- o service retorna a Observable do
HttpClient; - não existe
subscribe()escondido dentro do service; debounceTimeevita chamadas a cada tecla;distinctUntilChangedevita repetir o mesmo valor;switchMapmantém somente a busca mais recente relevante;- termos vazios ou muito curtos são tratados antes da API;
- erros da requisição não encerram a pesquisa;
- a inscrição principal termina ao destruir o componente;
switchMapnão é usado mecanicamente em gravações que não podem ser descartadas.
Próximo conteúdo do cluster Angular
Uma boa continuação é uma pesquisa completa com debounce + switchMap + loading + paginação, coordenando termo, filtros e página sem duplicar chamadas HTTP.