CodePráticov3.8.0
AngularRxJS

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.

Problema que vamos resolver: o usuário digita rápido, a aplicação dispara várias chamadas e uma resposta antiga pode chegar depois da mais recente. O resultado exibido pode ficar incorreto ou desatualizado.

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:

  1. o usuário altera o campo;
  2. debounceTime(400) aguarda 400 ms sem novas alterações;
  3. distinctUntilChanged() evita repetir a mesma pesquisa consecutivamente;
  4. switchMap() cancela a inscrição da busca anterior;
  5. 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.

Se você usa um loading global por interceptor: considere um contador de requisições em vez de um único booleano. Assim, uma finalização não esconde o loading enquanto outra chamada ainda estiver ativa.

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

Não use 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.

  • debounceTime reduz a quantidade de requisições;
  • switchMap impede 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;
  • debounceTime evita chamadas a cada tecla;
  • distinctUntilChanged evita repetir o mesmo valor;
  • switchMap manté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;
  • switchMap nã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.