Operador switchMap para búsquedas

Intermedio
Angular
Angular
Actualizado: 24/09/2025

SwitchMap básico

El operador switchMap es uno de los operadores de transformación más utilizados en RxJS, especialmente en aplicaciones Angular para manejar peticiones HTTP dinámicas. Su principal característica es que cancela automáticamente las suscripciones anteriores cuando llega un nuevo valor, manteniendo únicamente la suscripción más reciente.

¿Qué hace switchMap?

SwitchMap toma cada valor emitido por un observable fuente y lo transforma en un nuevo observable. La particularidad es que cuando llega un nuevo valor, automáticamente cancela el observable anterior y se suscribe solo al nuevo. Esto lo convierte en la opción ideal para escenarios donde solo nos interesa el resultado más reciente.

import { switchMap } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
import { HttpClient } from '@angular/common/http';

// El patrón básico de switchMap
searchTerm$.pipe(
  switchMap(term => this.http.get(`/api/search?q=${term}`))
).subscribe(results => {
  console.log(results);
});

Ejemplo práctico: búsqueda en tiempo real

Imagina un campo de búsqueda donde el usuario puede escribir y queremos mostrar resultados en tiempo real. Sin switchMap, cada tecla pulsada generaría una petición HTTP, y podrían llegar respuestas desordenadas si una petición anterior es más lenta que una posterior.

Componente de búsqueda:

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

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <div>
      <input 
        type="text" 
        [formControl]="searchControl"
        placeholder="Buscar productos...">
      
      @if (loading) {
        <p>Buscando...</p>
      }
      
      @if (results.length > 0) {
        <ul>
          @for (result of results; track result.id) {
            <li>{{ result.name }}</li>
          }
        </ul>
      }
    </div>
  `
})
export class SearchComponent {
  private http = inject(HttpClient);
  
  searchControl = new FormControl('');
  results: any[] = [];
  loading = false;

  ngOnInit() {
    this.searchControl.valueChanges.pipe(
      debounceTime(300), // Espera 300ms después de que el usuario deje de escribir
      distinctUntilChanged(), // Solo procede si el valor ha cambiado
      switchMap(searchTerm => {
        if (!searchTerm) {
          return [];
        }
        
        this.loading = true;
        return this.http.get<any[]>(`/api/products/search?q=${searchTerm}`);
      })
    ).subscribe(results => {
      this.results = results;
      this.loading = false;
    });
  }
}

¿Por qué es crucial la cancelación?

Sin switchMap, si el usuario escribe "Angular" rápidamente, se enviarían múltiples peticiones:

  • Petición para "A"
  • Petición para "An"
  • Petición para "Ang"
  • Petición para "Angu"
  • Petición para "Angul"
  • Petición para "Angular"

Si la petición para "Ang" es más lenta que la de "Angular", podríamos mostrar resultados incorrectos. SwitchMap soluciona esto cancelando automáticamente las peticiones anteriores, garantizando que solo vemos los resultados de la búsqueda más reciente.

Servicio de búsqueda

Podemos encapsular la lógica de búsqueda en un servicio:

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class SearchService {
  private http = inject(HttpClient);

  searchProducts(searchTerm$: Observable<string>): Observable<any[]> {
    return searchTerm$.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(term => {
        if (!term.trim()) {
          return [];
        }
        return this.http.get<any[]>(`/api/products/search?q=${term}`);
      })
    );
  }
}

Otros operadores de aplanamiento

Aunque switchMap es ideal para búsquedas, existen otros operadores de transformación como mergeMap y concatMap que manejan múltiples observables de manera diferente. Cada uno tiene casos de uso específicos que exploraremos en cursos más avanzados, pero para la mayoría de escenarios de búsqueda y peticiones dinámicas, switchMap es la opción más adecuada.

La clave está en recordar que switchMap siempre mantiene solo la suscripción más reciente, cancelando las anteriores automáticamente, lo que lo convierte en la herramienta perfecta para interfaces de usuario reactivas.

Casos de uso comunes

Aunque la búsqueda en tiempo real es el ejemplo más popular de switchMap, este operador resulta útil en múltiples escenarios donde necesitamos cancelar operaciones anteriores y mantener solo la más reciente.

Navegación dependiente entre selectores

Un patrón común es tener selectores cascada donde la selección de un elemento determina las opciones del siguiente. SwitchMap es perfecto para cancelar la carga de opciones anteriores cuando el usuario cambia la selección.

@Component({
  selector: 'app-location-selector',
  template: `
    <select [formControl]="countryControl">
      <option value="">Selecciona país</option>
      @for (country of countries; track country.id) {
        <option [value]="country.id">{{ country.name }}</option>
      }
    </select>

    <select [formControl]="cityControl" [disabled]="cities.length === 0">
      <option value="">Selecciona ciudad</option>
      @for (city of cities; track city.id) {
        <option [value]="city.id">{{ city.name }}</option>
      }
    </select>
  `
})
export class LocationSelectorComponent {
  private http = inject(HttpClient);
  
  countryControl = new FormControl('');
  cityControl = new FormControl('');
  
  countries: any[] = [];
  cities: any[] = [];

  ngOnInit() {
    // Cargar ciudades cuando cambie el país seleccionado
    this.countryControl.valueChanges.pipe(
      switchMap(countryId => {
        if (!countryId) return [];
        return this.http.get<any[]>(`/api/countries/${countryId}/cities`);
      })
    ).subscribe(cities => {
      this.cities = cities;
      this.cityControl.setValue(''); // Resetear selección de ciudad
    });
  }
}

Actualización de datos en tiempo real

Cuando necesitamos refrescar información basada en cambios del usuario, switchMap cancela las peticiones anteriores para evitar condiciones de carrera.

@Component({
  selector: 'app-user-profile',
  template: `
    <select [formControl]="userControl">
      @for (user of users; track user.id) {
        <option [value]="user.id">{{ user.name }}</option>
      }
    </select>
    
    @if (profile) {
      <div class="profile-card">
        <h3>{{ profile.name }}</h3>
        <p>Email: {{ profile.email }}</p>
        <p>Departamento: {{ profile.department }}</p>
      </div>
    }
  `
})
export class UserProfileComponent {
  private http = inject(HttpClient);
  
  userControl = new FormControl('');
  users: any[] = [];
  profile: any = null;

  ngOnInit() {
    this.userControl.valueChanges.pipe(
      switchMap(userId => {
        if (!userId) return [];
        // Cancela petición anterior si el usuario cambia rápidamente
        return this.http.get(`/api/users/${userId}/profile`);
      })
    ).subscribe(profile => {
      this.profile = profile;
    });
  }
}

Autoguardado de formularios

Para guardar cambios automáticamente mientras el usuario escribe, switchMap evita múltiples peticiones de guardado simultáneas.

@Component({
  selector: 'app-auto-save-form',
  template: `
    <textarea 
      [formControl]="contentControl"
      placeholder="Escribe aquí... (se guarda automáticamente)">
    </textarea>
    
    @if (saveStatus === 'saving') {
      <span class="status">Guardando...</span>
    }
    @if (saveStatus === 'saved') {
      <span class="status success">✓ Guardado</span>
    }
  `
})
export class AutoSaveFormComponent {
  private http = inject(HttpClient);
  
  contentControl = new FormControl('');
  saveStatus: 'idle' | 'saving' | 'saved' = 'idle';

  ngOnInit() {
    this.contentControl.valueChanges.pipe(
      debounceTime(1000), // Espera 1 segundo después de que el usuario pare de escribir
      switchMap(content => {
        this.saveStatus = 'saving';
        return this.http.put('/api/document/autosave', { content });
      })
    ).subscribe(() => {
      this.saveStatus = 'saved';
      // Volver a idle después de mostrar confirmación
      setTimeout(() => this.saveStatus = 'idle', 2000);
    });
  }
}

Filtrado dinámico de listas

Cuando tenemos filtros múltiples que afectan una lista de resultados, switchMap asegura que solo se muestre el resultado del último filtro aplicado.

@Component({
  selector: 'app-product-filter',
  template: `
    <input [formControl]="searchControl" placeholder="Buscar producto">
    <select [formControl]="categoryControl">
      <option value="">Todas las categorías</option>
      @for (category of categories; track category.id) {
        <option [value]="category.id">{{ category.name }}</option>
      }
    </select>
    
    <div class="products">
      @for (product of filteredProducts; track product.id) {
        <div class="product-card">
          <h4>{{ product.name }}</h4>
          <p>{{ product.category }}</p>
          <p>${{ product.price }}</p>
        </div>
      }
    </div>
  `
})
export class ProductFilterComponent {
  private http = inject(HttpClient);
  
  searchControl = new FormControl('');
  categoryControl = new FormControl('');
  
  categories: any[] = [];
  filteredProducts: any[] = [];

  ngOnInit() {
    // Combinar ambos filtros en un solo observable
    combineLatest([
      this.searchControl.valueChanges.pipe(startWith('')),
      this.categoryControl.valueChanges.pipe(startWith(''))
    ]).pipe(
      debounceTime(300),
      switchMap(([search, category]) => {
        // Construir parámetros de consulta
        const params = new URLSearchParams();
        if (search) params.set('search', search);
        if (category) params.set('category', category);
        
        return this.http.get<any[]>(`/api/products?${params.toString()}`);
      })
    ).subscribe(products => {
      this.filteredProducts = products;
    });
  }
}

Cuándo usar switchMap

SwitchMap es la opción ideal cuando:

  • Solo importa el resultado más reciente: búsquedas, filtros, selecciones dependientes
  • Necesitas cancelar operaciones anteriores: evitar datos obsoletos o condiciones de carrera
  • El usuario puede cambiar rápidamente su entrada: campos de texto, selectores múltiples
  • Quieres evitar peticiones HTTP innecesarias: autoguardado, validaciones en tiempo real

La cancelación automática de switchMap lo convierte en una herramienta esencial para crear interfaces de usuario reactivas y eficientes, donde la experiencia del usuario mejora significativamente al mostrar siempre información actualizada y relevante.

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en Angular

Documentación oficial de Angular
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Angular es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Angular

Explora más contenido relacionado con Angular y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el funcionamiento básico del operador switchMap en RxJS.
  • Aplicar switchMap para manejar búsquedas en tiempo real y evitar condiciones de carrera.
  • Implementar switchMap en componentes Angular para cancelar peticiones HTTP anteriores.
  • Identificar casos de uso comunes donde switchMap mejora la experiencia de usuario.
  • Diferenciar switchMap de otros operadores de aplanamiento como mergeMap y concatMap.