Operadores clave de RxJS que todo desarrollador de Angular debe conocer

⚡ ¿Usas Angular moderno (v17+)? Salta directamente a Conectar flujos de búsqueda de RxJS con Signals de Angular para ver
toSignal()y el nuevo flujo de control con@ify@foren acción.
RxJS tiene más de cien operadores, e intentar memorizarlos todos puede ser abrumador. En el día a día con Angular, solo necesitas unos 7 operadores clave para construir el 95% de las funcionalidades reactivas del mundo real:
debounceTime, distinctUntilChanged, filter, switchMap, map, tap y catchError.
En esta guía, te voy a mostrar cómo trabajan juntos estos operadores esenciales mientras construimos una funcionalidad de búsqueda de productos en tiempo real conectada a la API de DummyJSON.
La arquitectura: componente, servicio y flujo de datos
Nuestra funcionalidad de búsqueda se compone de dos partes:
SearchComponent: Captura lo que escribe el usuario y renderiza las tarjetas de productos.SearchService: Recibe los términos de búsqueda, aplica el retardo y el filtrado, consulta los datos y maneja los errores.
[Evento de teclado] ➡️ Subject.next() ➡️ debounceTime(400) ➡️ distinctUntilChanged() ➡️ filter(length > 2) ➡️ switchMap(HTTP) ➡️ map(res.products) ➡️ Vista
Con la arquitectura clara, veamos cómo implementar el servicio que orquesta todo este flujo.
1. Construir el servicio de búsqueda con operadores de RxJS
Aquí tienes la implementación completa del servicio combinando nuestros 7 operadores clave:
// search.service.ts
import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Subject, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, filter, map, switchMap, tap, catchError } from 'rxjs';
export interface Product {
id: number;
title: string;
thumbnail: string;
price: number;
}
interface ProductAPIResponse {
products: Product[];
total: number;
}
@Injectable({
providedIn: 'root',
})
export class SearchService {
private http = inject(HttpClient);
private readonly API_URL = 'https://dummyjson.com/products/search';
// 1. Subject para enviar las pulsaciones de teclado al pipeline
private searchTermSubject = new Subject<string>();
// 2. Flujo reactivo compuesto
readonly products$ = this.searchTermSubject.asObservable().pipe(
// ⏰ 1. Debounce: espera una pausa de 400ms al escribir
debounceTime(400),
// 🔍 2. Distinct: descarta si el término es idéntico a la emisión anterior
distinctUntilChanged(),
// ✂️ 3. Filter: busca solo si tiene al menos 3 caracteres
filter((term) => term.trim().length >= 3),
// 📝 4. Tap: efecto secundario para depuración o registro
tap((term) => console.log(`Searching API for: ${term}`)),
// 🔄 5. SwitchMap: cancela la petición HTTP previa en curso y cambia a la nueva
switchMap((term) => this.fetchProducts(term)),
// 🎯 6. Map: extrae el arreglo de productos de la respuesta
map((response) => response.products)
);
searchByText(term: string): void {
this.searchTermSubject.next(term);
}
private fetchProducts(term: string) {
return this.http.get<ProductAPIResponse>(`${this.API_URL}?q=${term}`).pipe(
// 🛡️ 7. CatchError: evita que el flujo se rompa ante un error HTTP
catchError((error) => {
console.error('Error fetching products:', error);
return of({ products: [], total: 0 }); // Respuesta vacía de respaldo
})
);
}
}2. Análisis paso a paso de los 7 operadores clave
Veamos en detalle por qué cada operador se coloca en este orden exacto:
debounceTime(400):- Limita los eventos rápidos. Si el usuario escribe "laptop" en 200ms, solo la palabra final "laptop" activa el resto del pipeline en lugar de lanzar 6 llamadas independientes a la API.
distinctUntilChanged():- Evita peticiones duplicadas. Si el usuario escribe "cat", presiona retroceso y vuelve a escribir la "t" antes de que termine el temporizador de debounce, el valor sigue siendo "cat", por lo que no se envía ninguna petición nueva.
filter((term) => term.length >= 3):- Ignora búsquedas de 1 o 2 caracteres para no saturar el backend con consultas demasiado amplias y pesadas.
tap((term) => ...):- Ejecuta efectos secundarios sin mutar los datos (como registrar logs, enviar analíticas o activar un indicador de carga) sin alterar los valores del flujo.
switchMap((term) => this.http.get(...)):- Resuelve condiciones de carrera (race conditions). Si el usuario busca "iphone" y de inmediato escribe "macbook",
switchMapcancela al instante la petición HTTP de "iphone" y pasa a procesar "macbook".
- Resuelve condiciones de carrera (race conditions). Si el usuario busca "iphone" y de inmediato escribe "macbook",
map((res) => res.products):- Transforma la respuesta cruda de la API
{ products: [...], total: 100 }directamente en el arregloProduct[]que necesita la plantilla.
- Transforma la respuesta cruda de la API
catchError(() => of(...)):- Se coloca dentro del observable interno de
switchMappara que, si ocurre un fallo de red, retorne un valor seguro de respaldo sin destruir el flujo principal exterior desearchTermSubject.
- Se coloca dentro del observable interno de
Ahora que el servicio procesa los términos de búsqueda de forma controlada, veamos cómo conectar este flujo reactivo en un componente de Angular moderno.
Conectar flujos de búsqueda de RxJS con Signals de Angular
En Angular moderno (v17+ y v19), puedes enlazar este flujo directamente con el flujo de control de la plantilla usando toSignal():
// search.component.ts (Standalone y Signals en Angular moderno)
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { SearchService } from './search.service';
@Component({
selector: 'app-search',
standalone: true,
template: `
<div class="search-container">
<h1>Product Live Search</h1>
<input
type="search"
placeholder="Search products (e.g. phone, laptop)..."
#searchInput
(input)="onSearch(searchInput.value)"
/>
<div class="results-grid">
@for (product of products(); track product.id) {
<div class="product-card">
<img [src]="product.thumbnail" [alt]="product.title" />
<h3>{{ product.title }}</h3>
<p>\${{ product.price }}</p>
</div>
} @empty {
<p class="empty-state">No products found. Start typing to search.</p>
}
</div>
</div>
`,
})
export class SearchComponent {
private searchService = inject(SearchService);
// Convierte el pipeline de RxJS en una Signal síncrona con un arreglo vacío inicial
products = toSignal(this.searchService.products$, { initialValue: [] });
onSearch(term: string): void {
this.searchService.searchByText(term);
}
}Tabla rápida de referencia de operadores
| Operador | Tipo | Caso de uso real |
|---|---|---|
debounceTime(ms) | Control de frecuencia (Rate Limiting) | Campos de búsqueda en vivo, eventos de redimensionado de ventana |
distinctUntilChanged() | Filtrado | Evitar llamadas duplicadas e innecesarias a la API |
filter(predicate) | Filtrado | Longitud mínima de búsqueda, validación de valores no nulos |
switchMap(project) | Transformación | Cancelar peticiones HTTP previas al buscar |
concatMap(project) | Transformación | Colas secuenciales (por ejemplo, subida ordenada de archivos) |
mergeMap(project) | Transformación | Peticiones concurrentes e independientes |
map(project) | Transformación | Extraer propiedades específicas de la respuesta de la API |
catchError(handler) | Manejo de errores | Proveer datos de respaldo ante un fallo HTTP |
Resumen
No necesitas memorizar cada operador de RxJS para escribir código limpio y reactivo en Angular. Conocer bien estos 7 operadores clave, y conectarlos con Signals en Angular moderno usando toSignal(), te da las herramientas necesarias para gestionar flujos de eventos asíncronos complejos de forma clara y mantenible.
These are my experiences learning and facing my daily challenges working with Angular.
Preguntas Frecuentes
¿Cuál es la diferencia entre switchMap y mergeMap en RxJS?
switchMap cancela el observable interno anterior cada vez que llega un nuevo valor de origen, por lo que es ideal para búsquedas en tiempo real donde solo importa la última pulsación de tecla. mergeMap mantiene todos los observables internos ejecutándose en paralelo, lo que resulta adecuado para subidas simultáneas de archivos.
¿Cómo funciona debounceTime y por qué es esencial en campos de búsqueda en Angular?
debounceTime retrasa la emisión hasta que pasa un periodo determinado de silencio sin nuevas emisiones. Esto evita lanzar una petición HTTP con cada pulsación de tecla, reduciendo la carga del servidor y previniendo límites de peticiones.
¿Qué hace distinctUntilChanged en un flujo de búsqueda con RxJS?
distinctUntilChanged filtra valores consecutivos idénticos. Si el usuario presiona retroceso y vuelve a escribir el mismo carácter antes de que expire debounceTime, evita llamadas duplicadas e innecesarias a la API.
¿Cómo conectar un flujo de búsqueda de RxJS con Signals en Angular moderno?
Puedes convertir el flujo final compuesto de RxJS en una Signal usando toSignal() de @angular/core/rxjs-interop, lo que permite lecturas síncronas y sin fugas de memoria en la plantilla, sin necesidad de usar el pipe async.
Artículos Relacionados
Cómo manejar y capturar errores en RxJS y Angular
Aprende a capturar, manejar y recuperarte de errores en flujos de RxJS y Angular usando catchError, throwError, EMPTY y la interoperabilidad con Signals en Angular moderno.
Cómo combinar Async Pipes en Angular: De combineLatest a Signals y @let
Aprende cómo evitar múltiples async pipes y suscripciones duplicadas en plantillas de Angular usando combineLatest, Signals modernas (toSignal) y la sintaxis @let.
Cómo simplificar suscripciones asíncronas anidadas en Angular: Un ejemplo práctico
Elimina las suscripciones asíncronas anidadas y el callback hell en plantillas de Angular usando operadores de aplanamiento de RxJS (switchMap, combineLatest), declaraciones @let y toSignal().
Cómo compartir datos entre componentes en Angular (Signals y servicios)
Aprende los mejores patrones para compartir datos entre componentes en Angular. Compara los decoradores clásicos Input/Output y BehaviorSubjects con las Signals modernas: input(), output(), model() y servicios basados en Signals.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.