🇺🇸 EN
Angular
·6 min read·↗·🇺🇸 EN

Operadores clave de RxJS que todo desarrollador de Angular debe conocer

Portada principal del artículo: 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 @if y @for en 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:

  1. SearchComponent: Captura lo que escribe el usuario y renderiza las tarjetas de productos.
  2. 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:

  1. 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.
  2. 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.
  3. filter((term) => term.length >= 3):
    • Ignora búsquedas de 1 o 2 caracteres para no saturar el backend con consultas demasiado amplias y pesadas.
  4. 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.
  5. switchMap((term) => this.http.get(...)):
    • Resuelve condiciones de carrera (race conditions). Si el usuario busca "iphone" y de inmediato escribe "macbook", switchMap cancela al instante la petición HTTP de "iphone" y pasa a procesar "macbook".
  6. map((res) => res.products):
    • Transforma la respuesta cruda de la API { products: [...], total: 100 } directamente en el arreglo Product[] que necesita la plantilla.
  7. catchError(() => of(...)):
    • Se coloca dentro del observable interno de switchMap para que, si ocurre un fallo de red, retorne un valor seguro de respaldo sin destruir el flujo principal exterior de searchTermSubject.

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

OperadorTipoCaso de uso real
debounceTime(ms)Control de frecuencia (Rate Limiting)Campos de búsqueda en vivo, eventos de redimensionado de ventana
distinctUntilChanged()FiltradoEvitar llamadas duplicadas e innecesarias a la API
filter(predicate)FiltradoLongitud mínima de búsqueda, validación de valores no nulos
switchMap(project)TransformaciónCancelar peticiones HTTP previas al buscar
concatMap(project)TransformaciónColas secuenciales (por ejemplo, subida ordenada de archivos)
mergeMap(project)TransformaciónPeticiones concurrentes e independientes
map(project)TransformaciónExtraer propiedades específicas de la respuesta de la API
catchError(handler)Manejo de erroresProveer 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.

Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

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

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.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.