💻 Exemplos: Capítulo 11: Operadores Essenciais do RxJS (switchMap, debounceTime, catchError)
Especialização em Front-End com Angular • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 11 — Busca Assíncrona Reativa com debounceTime, switchMap e catchError
Este exemplo apresenta a construção da pipeline clássica de busca assíncrona reativa no Angular 18+, utilizando operadores essenciais do RxJS para controlar a taxa de digitação (debounceTime), filtrar termos repetidos (distinctUntilChanged), cancelar requisições anteriores obsoletas (switchMap) e capturar falhas com elegância (catchError).
// src/app/components/enterprise-search-box.component.ts
import { Component, signal } from '@angular/core';
import { CommonModule, AsyncPipe } from '@angular/common';
import { Subject, of, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, catchError, tap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
interface SearchResultItem {
id: string;
name: string;
category: string;
latencyMs: number;
}
@Component({
selector: 'app-enterprise-search-box',
standalone: true,
imports: [CommonModule, AsyncPipe],
template: `
<div class="max-w-xl mx-auto p-6 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-5">
<div class="border-b pb-3">
<h2 class="text-xl font-bold text-slate-800">🔍 Pesquisa Reativa com Operadores RxJS</h2>
<p class="text-xs text-slate-500">Pipeline: debounceTime(350) ➔ distinctUntilChanged ➔ switchMap</p>
</div>
<!-- Campo de Digitação Conectado ao Subject -->
<div class="relative">
<input
type="text"
#searchInput
(input)="onSearchInput(searchInput.value)"
placeholder="Digite o nome de um serviço ou nó..."
class="w-full border px-4 py-2.5 rounded-xl text-sm outline-none focus:ring-2 focus:ring-red-500 pl-10"
/>
<span class="absolute left-3.5 top-3 text-slate-400 text-sm">🔎</span>
</div>
<!-- Indicador de Carregamento (Loading Signal) -->
@if (isLoading()) {
<div class="flex items-center gap-2 text-xs text-slate-500 py-1">
<span class="w-3.5 h-3.5 border-2 border-red-600 border-t-transparent rounded-full animate-spin"></span>
<span>Consultando catálogo de serviços via switchMap...</span>
</div>
}
<!-- Erro Tratado via catchError -->
@if (errorMessage()) {
<div class="p-3 bg-red-50 border border-red-200 text-red-700 text-xs rounded-xl">
⚠️ {{ errorMessage() }}
</div>
}
<!-- Resultados Consumidos com AsyncPipe -->
<div class="space-y-2">
<h4 class="text-xs font-bold uppercase text-slate-400">Resultados Encontrados:</h4>
<div class="border rounded-xl divide-y overflow-hidden max-h-56 overflow-y-auto">
@if (searchResults$ | async; as results) {
@for (item of results; track item.id) {
<div class="p-3 flex justify-between items-center hover:bg-slate-50 text-xs">
<div>
<span class="font-bold text-slate-800">{{ item.name }}</span>
<span class="ml-2 text-[10px] px-2 py-0.5 rounded bg-slate-100 text-slate-600 uppercase font-mono">
{{ item.category }}
</span>
</div>
<span class="font-mono text-slate-400 text-[11px]">{{ item.latencyMs }} ms</span>
</div>
} @empty {
<div class="p-4 text-center text-xs text-slate-400">
Nenhum serviço atende ao termo pesquisado.
</div>
}
}
</div>
</div>
</div>
`
})
export class EnterpriseSearchBoxComponent {
// 1. Subject para emissão de termos digitados pelo usuário
private searchTerm$ = new Subject<string>();
isLoading = signal<boolean>(false);
errorMessage = signal<string | null>(null);
// 2. Pipeline Reativa Encadeada
readonly searchResults$: Observable<SearchResultItem[]> = this.searchTerm$.pipe(
// A. Aguarda 350ms de pausa na digitação do usuário
debounceTime(350),
// B. Ignora se o termo for idêntico ao anterior (ex: pressionar Shift ou setas)
distinctUntilChanged(),
// C. Ativa indicador de loading
tap(() => {
this.isLoading.set(true);
this.errorMessage.set(null);
}),
// D. switchMap: Cancela a requisição anterior se o usuário digitar algo novo!
switchMap((term) =>
this.mockApiSearch(term).pipe(
// E. catchError: Trata exceções sem matar o stream principal da busca
catchError((err) => {
this.errorMessage.set(`Erro na consulta: ${err.message}`);
return of([]); // Retorna array vazio seguro
})
)
),
// F. Desativa loading após resolução
tap(() => this.isLoading.set(false)),
// G. takeUntilDestroyed: Cancela todo o stream ao desmontar o componente
takeUntilDestroyed()
);
onSearchInput(value: string): void {
this.searchTerm$.next(value.trim());
}
// Simulação de chamada HTTP com latência e cancelamento
private mockApiSearch(term: string): Observable<SearchResultItem[]> {
if (!term) return of([]);
return new Observable((observer) => {
console.log(`[API Search] Disparando requisição HTTP para termo: "${term}"`);
const timer = setTimeout(() => {
if (term.toLowerCase() === 'erro') {
observer.error(new Error('Falha de timeout no cluster de pesquisa.'));
return;
}
const mockDatabase: SearchResultItem[] = [
{ id: 'srv-01', name: 'Servidor de Autenticação OAuth2', category: 'Security', latencyMs: 24 },
{ id: 'srv-02', name: 'Gateway de Pagamentos PIX / TED', category: 'Finance', latencyMs: 18 },
{ id: 'srv-03', name: 'Cluster de Mensageria Kafka', category: 'Streaming', latencyMs: 42 },
{ id: 'srv-04', name: 'Serviço de Auditoria Fiscal', category: 'Compliance', latencyMs: 31 }
];
const filtered = mockDatabase.filter((i) =>
i.name.toLowerCase().includes(term.toLowerCase())
);
observer.next(filtered);
observer.complete();
}, 500);
// Função de Teardown: invocada pelo switchMap quando cancela a requisição!
return () => {
console.log(`[API Search] Requisição anterior para "${term}" foi cancelada pelo switchMap.`);
clearTimeout(timer);
};
});
}
}
🖥️ Saída / Comportamento Esperado
Cenário 1 — Digitação Rápida:
O usuário digita rapidamente "Serviço":
O operador debounceTime(350) aguarda a pausa do usuário.
Apenas uma única chamada HTTP é disparada ao término dos 350ms.
Cenário 2 — Cancelamento de Race Conditions com switchMap:
O usuário digita "Servidor" e, 200ms depois (antes da API responder), digita "Cluster":
Console:
"[API Search] Requisição anterior para 'Servidor' foi cancelada pelo switchMap."
"[API Search] Disparando requisição HTTP para termo: 'Cluster'"
Apenas o resultado do termo mais recente ("Cluster de Mensageria Kafka") é renderizado.
Cenário 3 — Tratamento de Erro com catchError:
O usuário digita "erro":
catchError intercepta o erro, alimenta errorMessage() e retorna of([]).
O stream continua vivo e pronto para novas pesquisas quando o usuário apagar o texto.
🔍 Destaques da Implementação
switchMapAnti-Race-Condition: Cancela o Observable interno anterior chamando sua função de teardown imediatamente.catchErrorcom Fallback: Impede que um erro HTTP 500 encerre definitivamente o Subject da barra de busca.distinctUntilChanged: Evita disparar requisições inúteis quando teclas de navegação ou modificadores são pressionados.