Capítulo 11: Operadores Essenciais do RxJS (map, filter, switchMap, debounceTime, catchError)

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual dos 4 Operadores de Mapeamento de Ordem Superior

flowchart TD
    In["Stream de Entrada: Eventos / Requisições"] --> Pipe["Pipeline .pipe()"]

    subgraph FLATTENING["Estratégias de Flattening (Ordem Superior)"]
        SM["switchMap: Cancela a requisição anterior e foca na mais recente"]
        CM["concatMap: Enfileira e executa estritamente em sequência"]
        MM["mergeMap: Executa todas simultaneamente em paralelo"]
        EM["exhaustMap: Ignora novas emissões até que a atual termine"]
    end

    Pipe --> FLATTENING

    SM -->|"Casos de Uso"| U1["Busca com Autocomplete / Typeahead"]
    CM -->|"Casos de Uso"| U2["Filas de Upload / Transações Contábeis"]
    MM -->|"Casos de Uso"| U3["Downloads em Lote Não Ordenados"]
    EM -->|"Casos de Uso"| U4["Botão de 'Pagar Agora' (Anti-Duplo Clique)"]

    style In fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style Pipe fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style FLATTENING fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style U1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style U4 fill:#fce4ec,stroke:#e91e63,stroke-width:2px

🏛️ 1. O Conceito de Operadores Encadeáveis (Pipeable Operators)

Os operadores do RxJS são funções puras que recebem um Observable de entrada, aplicam transformações, filtros ou orquestrações complexas, e retornam um novo Observable. Eles são encadeados usando a função .pipe(...).

Ao combinar operadores, conseguimos resolver problemas de concorrência e processamento assíncrono em poucas linhas de código declarativo que, de outra forma, exigiriam dezenas de ifs, flags e timeouts manuais propensos a race conditions.


⚙️ 2. Operadores de Filtragem e Transformação Básicos

2.1 map e filter

Transformam e filtram os dados emitidos pela stream:

import { of } from 'rxjs';
import { map, filter } from 'rxjs/operators';

of(1, 2, 3, 4, 5, 6).pipe(
  filter(n => n % 2 === 0), // Permite apenas pares: 2, 4, 6
  map(n => n * 10)          // Multiplica por 10: 20, 40, 60
).subscribe(console.log);

2.2 debounceTime e distinctUntilChanged

Essenciais para campos de busca e eventos de teclado:

  • debounceTime(300): Aguarda 300 milissegundos de silêncio antes de emitir o último caractere digitado.
  • distinctUntilChanged(): Impede que a mesma palavra de busca seja emitida duas vezes consecutivas (ex: se o usuário pressionar a tecla Ctrl ou seta).

🔄 3. Operadores de Achatamento: Os “Quatro Cavaleiros” do RxJS

Quando o retorno de uma operação é outro Observable (exemplo: uma chamada HTTP), precisamos “achatar” (flatten) as streams internas:

3.1 switchMap (Busca e Autocomplete)

Cancela imediatamente a requisição HTTP em andamento caso o usuário digite um novo caractere, evitando condições de corrida (race conditions):

import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-busca-livros',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="termoBusca" placeholder="Buscar livros..." />
  `
})
export class BuscaLivrosComponent {
  private http = inject(HttpClient);
  termoBusca = new FormControl('');

  constructor() {
    this.termoBusca.valueChanges.pipe(
      debounceTime(400),
      distinctUntilChanged(),
      switchMap(termo => {
        if (!termo || termo.trim().length < 3) return of([]);
        return this.http.get<any[]>(`https://api.livros.com/search?q=${termo}`).pipe(
          catchError(() => of([])) // Se a busca falhar, retorna array vazio sem matar a stream do input
        );
      }),
      takeUntilDestroyed()
    ).subscribe(resultados => {
      console.log('Resultados recebidos da API:', resultados);
    });
  }
}

3.2 concatMap (Ordem Estrita e Filas)

Processa uma requisição por vez, mantendo rigorosamente a ordem em que foram disparadas. Ideal para uploads sequenciais de arquivos ou transações financeiras.

3.3 exhaustMap (Prevenção de Cliques Múltiplos)

Descarta completamente novas requisições enquanto a requisição atual estiver em andamento. Padrão ouro para botões de login ou “Finalizar Pagamento”:

import { Subject } from 'rxjs';
import { exhaustMap } from 'rxjs/operators';

const cliquePagar$ = new Subject<void>();

cliquePagar$.pipe(
  exhaustMap(() => servicoPagamento.processarCobranca())
).subscribe(recibo => {
  console.log('Pagamento efetuado:', recibo);
});

🛡️ 4. Tratamento de Erros e Resiliência: catchError e retry

Em RxJS, se um Observable emite um erro não tratado, a stream morre permanentemente. Para manter a aplicação resiliente:

import { catchError, retry } from 'rxjs/operators';
import { throwError, of } from 'rxjs';

this.http.get('/api/dados').pipe(
  // Tenta novamente até 2 vezes antes de desistir
  retry({ count: 2, delay: 1000 }),
  
  // Intercepta e recupera o fluxo
  catchError(erro => {
    console.error('Falha irrecuperável na API:', erro);
    // Retorna um valor fallback ou relança o erro tratado
    return of({ status: 'offline', dados: [] });
  })
);

⚖️ 5. Comparativo: Quando Usar Cada Operador de Mapeamento

Operador Comportamento com Novas Emissões Caso de Uso Ideal
switchMap Cancela a anterior e roda a nova Autocomplete, abas/tabs ativas, rotas
concatMap Enfileira em sequência FIFO Upload ordenado, logs em lote
mergeMap Executa em paralelo simultaneamente Fetch de múltiplos IDs sem dependência
exhaustMap Ignora novas até terminar a atual Submissão de formulário, botões de checkout

⚠️ 6. Armadilhas Comuns e Anti-Patterns

  1. Colocar catchError no Local Errado Dentro de um switchMap:
    • Erro: Colocar o catchError no pipe exterior do formulário. Se a chamada HTTP falhar, a stream do input é encerrada e o usuário não consegue mais digitar nem buscar nada.
    • Correção: Coloque o catchError no pipe interno da chamada HTTP (this.http.get().pipe(catchError(...))), retornando of([]).
  2. Usar mergeMap para Operações Dependentes de Ordem:
    • Como o mergeMap roda chamadas em paralelo, uma requisição mais rápida pode sobrescrever uma mais lenta, causando inconsistência de dados.

📋 7. Cheat-Sheet: Resumo Executivo

stream$.pipe(
  debounceTime(300),              // Espera silêncio de 300ms
  distinctUntilChanged(),         // Ignora se for idêntico ao anterior
  switchMap(q => buscarApi(q)),   // Cancela chamadas velhas
  retry(2),                       // Retenta até 2 vezes
  catchError(e => of(fallback))   // Trata sem matar a stream
);