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
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.