La confusión de selección de operadores
RxJS tiene más de 100 tipos de operadores, y dudar sobre cuál usar es una dificultad que todos experimentan. Esta página proporciona criterios de selección prácticos y diagramas de flujo.
Criterios para elegir entre más de 100 operadores
Problema: Demasiadas opciones
// Quiero transformar un array... ¿map? ¿scan? ¿reduce? ¿toArray?
// Quiero llamar múltiples APIs... ¿mergeMap? ¿switchMap? ¿concatMap? ¿exhaustMap?
// Quiero filtrar valores... ¿filter? ¿take? ¿first? ¿distinctUntilChanged?
// Quiero combinar múltiples streams... ¿merge? ¿combineLatest? ¿zip? ¿forkJoin?Solución: Acotar por categoría + propósito
Diagrama de flujo de selección más detallado
El siguiente diagrama de flujo muestra el procedimiento para elegir operadores según propósitos específicos.
1. Operadores de transformación (Transformation)
¿Cuándo usar? Cuando quieres cambiar la forma de los datos, llamar procesamiento asíncrono
| Operador | Propósito | Casos de uso comunes |
|---|---|---|
| map | Transformación 1:1 de valores | Obtener propiedades, cálculos, conversión de tipos |
| scan | Procesamiento acumulativo (fluye valores intermedios) | Contador, suma, historial |
| reduce | Procesamiento acumulativo (solo valor final) | Suma de array, valor máximo |
| mergeMap | Ejecución paralela de procesamiento asíncrono | Llamadas paralelas a múltiples APIs |
| switchMap | Cambiar procesamiento asíncrono | API de búsqueda (solo el más reciente) |
| concatMap | Ejecución secuencial de procesamiento asíncrono | Procesamiento donde el orden es importante |
| exhaustMap | Ignorar nuevo procesamiento durante ejecución | Prevención de clics múltiples (botón enviar) |
Ejemplo práctico: Selección por caso de uso
Caso de uso 1: Obtener propiedad
import { of } from 'rxjs';
import { map } from 'rxjs';
interface User { id: number; name: string; }
of({ id: 1, name: 'Alice' }).pipe(
map(user => user.name) // Transformación 1:1 de valor map
).subscribe(name => console.log(name)); // 'Alice'Caso de uso 2: Contador
import { fromEvent } from 'rxjs';
import { scan } from 'rxjs';
const button = document.querySelector('button')!;
fromEvent(button, 'click').pipe(
scan(count => count + 1, 0) // Procesamiento acumulativo scan
).subscribe(count => console.log(`Número de clics: ${count}`));Caso de uso 3: Llamada a API de búsqueda
import { fromEvent } from 'rxjs';
import { debounceTime, map, switchMap } from 'rxjs';
const searchInput = document.querySelector('input')!;
fromEvent(searchInput, 'input').pipe(
debounceTime(300),
map(e => (e.target as HTMLInputElement).value),
switchMap(query => searchAPI(query)) // Solo el más reciente switchMap
).subscribe(results => console.log(results));2. Operadores de filtrado (Filtering)
¿Cuándo usar?
Cuando quieres seleccionar valores, controlar el timing
| Operador | Propósito | Casos de uso comunes |
|---|---|---|
| filter | Pasar solo valores que cumplen condición | Solo números pares, solo valores no nulos |
| take | Solo los primeros N | Obtener primeros 5 elementos |
| first | Solo el primero | Obtener valor inicial |
| distinctUntilChanged | Solo valores diferentes del anterior | Excluir duplicados |
| debounceTime | Emitir después de tiempo transcurrido | Entrada de búsqueda (después de completar entrada) |
| throttleTime | Reducir a intervalos regulares | Evento de scroll |
Ejemplo práctico: Selección por caso de uso
Caso de uso 1: Obtener solo números pares
import { of } from 'rxjs';
import { filter } from 'rxjs';
of(1, 2, 3, 4, 5).pipe(
filter(n => n % 2 === 0) // Solo valores que cumplen condición filter
).subscribe(console.log); // 2, 4Caso de uso 2: Optimización de entrada de búsqueda
import { fromEvent } from 'rxjs';
import { debounceTime, distinctUntilChanged, map } from 'rxjs';
const input = document.querySelector('input')!;
fromEvent(input, 'input').pipe(
debounceTime(300), // Esperar finalización de entrada debounceTime
map(e => (e.target as HTMLInputElement).value),
distinctUntilChanged() // Excluir duplicados distinctUntilChanged
).subscribe(query => console.log('Búsqueda:', query));Caso de uso 3: Reducción de eventos de scroll
import { fromEvent } from 'rxjs';
import { throttleTime } from 'rxjs';
fromEvent(window, 'scroll').pipe(
throttleTime(200) // Solo una vez cada 200ms throttleTime
).subscribe(() => console.log('Posición de scroll:', window.scrollY));3. Operadores de combinación (Combination)
¿Cuándo usar?
Cuando quieres combinar múltiples streams
| Operador | Propósito | Casos de uso comunes |
|---|---|---|
| merge | Múltiples streams en paralelo | Monitoreo de múltiples eventos |
| combineLatest | Combinar todos los valores más recientes | Validación de formularios |
| zip | Emparejar valores correspondientes | Relacionar resultados de 2 APIs |
| forkJoin | Resultados en array después de finalización completa | Ejecución paralela de múltiples APIs |
| withLatestFrom | Stream principal + valor auxiliar | Evento + estado actual |
Ejemplo práctico: Selección por caso de uso
Caso de uso 1: Monitorear múltiples eventos
import { fromEvent, merge } from 'rxjs';
const clicks$ = fromEvent(document, 'click');
const keypresses$ = fromEvent(document, 'keypress');
merge(clicks$, keypresses$).pipe() // Monitoreo paralelo merge
.subscribe(() => console.log('Ocurrió algún evento'));Caso de uso 2: Validación de formularios
import { combineLatest } from 'rxjs';
import { map } from 'rxjs';
const email$ = getFormControl('email');
const password$ = getFormControl('password');
combineLatest([email$, password$]).pipe( // Todos los valores más recientes combineLatest
map(([email, password]) => email.length > 0 && password.length > 7)
).subscribe(isValid => console.log('Formulario válido:', isValid));Caso de uso 3: Ejecución paralela de múltiples APIs
import { forkJoin } from 'rxjs';
forkJoin({
user: getUserAPI(),
posts: getPostsAPI(),
comments: getCommentsAPI()
}).subscribe(({ user, posts, comments }) => { // Espera finalización completa forkJoin
console.log('Obtención completa de todos los datos', { user, posts, comments });
});Los 20 operadores más usados
Los siguientes son los operadores más utilizados frecuentemente en el trabajo. Primero domina estos 20.
>G Más frecuentes (obligatorios)
- map - Transformar valores
- filter - Filtrar por condición
- switchMap - Búsqueda, etc., solo necesario el más reciente
- tap - Depuración, efectos secundarios
- take - Primeros N
- first - Primero 1
- catchError - Manejo de errores
- takeUntil - Cancelar suscripción
>H Frecuentes (uso común)
- mergeMap - Procesamiento asíncrono paralelo
- debounceTime - Esperar finalización de entrada
- distinctUntilChanged - Excluir duplicados
- combineLatest - Combinar múltiples valores
- startWith - Establecer valor inicial
- scan - Procesamiento acumulativo
- shareReplay - Cachear resultados
>I Uso común (deberías conocer)
- concatMap - Procesamiento secuencial
- throttleTime - Reducción de eventos
- withLatestFrom - Obtener valor auxiliar
- forkJoin - Espera de múltiples APIs
- retry - Procesamiento de reintento
switchMap vs mergeMap vs concatMap vs exhaustMap
Estos 4 son los operadores más confundidos. Entendamos claramente sus diferencias.
Tabla comparativa
| Operador | Método de ejecución | Procesamiento anterior | Nuevo procesamiento | Dónde usar |
|---|---|---|---|---|
| switchMap | Cambiar | Cancelar | Iniciar inmediatamente | Búsqueda, autocompletar |
| mergeMap | Ejecución paralela | Continuar | Iniciar inmediatamente | Subida de archivos, análisis |
| concatMap | Ejecución secuencial | Esperar finalización | Iniciar después de esperar | Procesamiento donde el orden es importante |
| exhaustMap | Ignorar durante ejecución | Continuar | Ignorar | Prevención de clics múltiples en botón |
Comparación con diagramas de mármol
Exterior: ----A----B----C----|
Interior: A --1--2|
B --3--4|
C --5--6|
switchMap: ----1--3--5--6| (A se cancela antes de 2, B se cancela antes de 4)
mergeMap: ----1-23-45-6| (todo ejecución paralela)
concatMap: ----1--2--3--4--5--6| (ejecución secuencial)
exhaustMap: ----1--2| (B, C se ignoran)Ejemplo práctico: Diferencia de los 4 en el mismo procesamiento
Situación: Llamar API (tarda 1 segundo) con cada clic del botón. Usuario hace clic cada 0.5 segundos.
switchMap - Óptimo para búsqueda
import { fromEvent } from 'rxjs';
import { switchMap } from 'rxjs';
fromEvent(button, 'click').pipe(
switchMap(() => searchAPI()) // Solo ejecutar el más reciente, cancelar solicitudes antiguas
).subscribe(result => console.log(result));
// 0.0 seg: Clic1 Inicio API1
// 0.5 seg: Clic2 Cancelar API1, Inicio API2
// 1.0 seg: Clic3 Cancelar API2, Inicio API3
// 2.0 seg: Completar API3 Mostrar resultado (solo API3)=¡ Dónde usar
- Búsqueda/autocompletar: Solo necesario el valor de entrada más reciente
- Cambio de pestañas: Solo necesarios datos de la pestaña mostrada
- Paginación: Solo mostrar la página más reciente
mergeMap - Óptimo para procesamiento paralelo
import { fromEvent } from 'rxjs';
import { mergeMap } from 'rxjs';
fromEvent(button, 'click').pipe(
mergeMap(() => uploadFileAPI()) // Todo ejecución paralela
).subscribe(result => console.log(result));
// 0.0 seg: Clic1 Inicio API1
// 0.5 seg: Clic2 Inicio API2 (API1 continúa)
// 1.0 seg: Clic3 Inicio API3 (API1, API2 continúan)
// 1.0 seg: Completar API1 Mostrar resultado
// 1.5 seg: Completar API2 Mostrar resultado
// 2.0 seg: Completar API3 Mostrar resultado=¡ Dónde usar
- Subida de archivos: Subir múltiples archivos simultáneamente
- Análisis/envío de logs: Ejecutar procesamientos independientes en paralelo
- Sistema de notificaciones: Procesar múltiples notificaciones simultáneamente
concatMap - Óptimo para procesamiento donde el orden es importante
import { fromEvent } from 'rxjs';
import { concatMap } from 'rxjs';
fromEvent(button, 'click').pipe(
concatMap(() => updateDatabaseAPI()) // Ejecución secuencial (esperar finalización anterior)
).subscribe(result => console.log(result));
// 0.0 seg: Clic1 Inicio API1
// 0.5 seg: Clic2 Esperar (añadir a cola)
// 1.0 seg: Clic3 Esperar (añadir a cola)
// 1.0 seg: Completar API1 Mostrar resultado, Inicio API2
// 2.0 seg: Completar API2 Mostrar resultado, Inicio API3
// 3.0 seg: Completar API3 Mostrar resultado=¡ Dónde usar
- Actualización de base de datos: Procesamiento de escritura donde el orden es importante
- Transacciones: Usar resultado del procesamiento anterior en el siguiente
- Animaciones: Procesos que quieres ejecutar en orden
exhaustMap - Óptimo para prevención de clics múltiples
import { fromEvent } from 'rxjs';
import { exhaustMap } from 'rxjs';
fromEvent(button, 'click').pipe(
exhaustMap(() => submitFormAPI()) // Ignorar nuevas solicitudes durante ejecución
).subscribe(result => console.log(result));
// 0.0 seg: Clic1 Inicio API1
// 0.5 seg: Clic2 Ignorar (API1 en ejecución)
// 1.0 seg: Clic3 Ignorar (API1 en ejecución)
// 1.0 seg: Completar API1 Mostrar resultado
// 1.5 seg: Clic4 Inicio API4 (anterior ya completado)=¡ Dónde usar
- Botón enviar: Prevención de envío doble
- Procesamiento de login: Prevenir errores por clics múltiples
- Procesamiento de pago: Prevenir ejecución duplicada
Diagrama de flujo de selección
Criterios de decisión en la práctica
Paso 1: Clarificar qué quieres lograr
// L Mal ejemplo: Usar mergeMap sin más
observable$.pipe(
mergeMap(value => someAPI(value))
);
// Buen ejemplo: Elegir después de clarificar el propósito
// Propósito: Para la entrada de búsqueda del usuario, quiero mostrar solo el resultado más reciente
// Debería cancelar solicitudes antiguas switchMap
searchInput$.pipe(
switchMap(query => searchAPI(query))
);Paso 2: Considerar el rendimiento
Elección de debounceTime vs throttleTime
// Entrada de búsqueda: Ejecutar después de que el usuario "complete" la entrada
searchInput$.pipe(
debounceTime(300), // Ejecutar si no hay entrada durante 300ms
switchMap(query => searchAPI(query))
);
// Scroll: Ejecutar a intervalos regulares (prevenir alta frecuencia)
scroll$.pipe(
throttleTime(200), // Solo ejecutar una vez cada 200ms
tap(() => loadMoreItems())
);Paso 3: Incorporar manejo de errores
import { of } from 'rxjs';
import { catchError, retry, switchMap } from 'rxjs';
searchInput$.pipe(
debounceTime(300),
switchMap(query =>
searchAPI(query).pipe(
retry(2), // Reintentar hasta 2 veces
catchError((err: unknown) => {
console.error('Error de búsqueda:', err);
return of([]); // Devolver array vacío
})
)
)
).subscribe(results => console.log(results));Paso 4: Prevenir fugas de memoria
import { Subject } from 'rxjs';
import { switchMap, takeUntil } from 'rxjs';
class SearchComponent {
private destroy$ = new Subject<void>();
ngOnInit() {
searchInput$.pipe(
debounceTime(300),
switchMap(query => searchAPI(query)),
takeUntil(this.destroy$) // Cancelar al destruir componente
).subscribe(results => console.log(results));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}Lista de verificación de comprensión
Verifica si puedes responder a las siguientes preguntas.
## Comprensión básica
- [ ] Puedo clasificar operadores por categoría (transformación, filtrado, combinación)
- [ ] Puedo explicar más de 10 de los 20 operadores más usados
- [ ] Puedo explicar las diferencias entre switchMap, mergeMap, concatMap, exhaustMap
## Selección práctica
- [ ] Puedo elegir operadores adecuados para función de búsqueda (switchMap + debounceTime)
- [ ] Puedo elegir operadores adecuados para llamadas paralelas a múltiples APIs (forkJoin or mergeMap)
- [ ] Puedo elegir operadores adecuados para validación de formularios (combineLatest)
## Rendimiento
- [ ] Puedo diferenciar el uso de debounceTime y throttleTime
- [ ] Conozco métodos de optimización para eventos de alta frecuencia
- [ ] Puedo implementar patrones para prevenir fugas de memoria
## Manejo de errores
- [ ] Puedo usar catchError y retry en combinación
- [ ] Puedo implementar procesamiento de fallback en caso de error
- [ ] Puedo dar feedback de errores al usuarioPróximos pasos
Una vez que entiendas la selección de operadores, aprende sobre timing y orden.
Comprensión de timing y orden (en preparación) - Cuándo fluyen los valores, comprensión de sincronía vs asincronía
Páginas relacionadas
- Chapter 4: Comprensión de operadores - Detalles de todos los operadores
- Chapter 13: Colección de patrones prácticos - Casos de uso reales (en preparación)
- Chapter 10: Errores comunes y soluciones - Antipatrones de selección inadecuada de operadores
<¯ Ejercicios de práctica
Problema 1: Seleccionar operador adecuado
Elige el operador más óptimo para los siguientes escenarios.
- Usuario ingresa en caja de búsqueda Llamada a API
- Clic de botón para subir múltiples archivos
- Determinar si todos los campos del formulario son válidos
- Prevenir clics múltiples en botón enviar
Ejemplo de respuesta
1. Caja de búsqueda Llamada a API
searchInput$.pipe(
debounceTime(300), // Esperar finalización de entrada
distinctUntilChanged(), // Excluir duplicados
switchMap(query => searchAPI(query)) // Solo el más reciente
).subscribe(results => displayResults(results));Razón
La búsqueda solo necesita el resultado más reciente, por lo que switchMap. Espera finalización de entrada con debounceTime.
2. Subir múltiples archivos
fromEvent(uploadButton, 'click').pipe(
mergeMap(() => {
const files = getSelectedFiles();
return forkJoin(files.map(file => uploadFileAPI(file)));
})
).subscribe(results => console.log('Subida completa de todos los archivos', results));Razón
Para subir múltiples archivos en paralelo, forkJoin. También es posible mergeMap para procesamientos independientes.
3. Validez de todos los campos del formulario
combineLatest([
emailField$,
passwordField$,
agreeTerms$
]).pipe(
map(([email, password, agreed]) =>
email.valid && password.valid && agreed
)
).subscribe(isValid => submitButton.disabled = !isValid);Razón
Para combinar los valores más recientes de todos los campos, combineLatest.
4. Prevención de clics múltiples en botón enviar
fromEvent(submitButton, 'click').pipe(
exhaustMap(() => submitFormAPI())
).subscribe(result => console.log('Envío completado', result));Razón
Para proteger el procesamiento en ejecución e ignorar nuevos clics, exhaustMap.
Problema 2: Elección de switchMap y mergeMap
El siguiente código usa mergeMap, pero hay un problema. Corrígelo.
searchInput$.pipe(
debounceTime(300),
mergeMap(query => searchAPI(query))
).subscribe(results => displayResults(results));Ejemplo de respuesta
searchInput$.pipe(
debounceTime(300),
switchMap(query => searchAPI(query)) // mergeMap switchMap
).subscribe(results => displayResults(results));Problema
- Con
mergeMap, todas las solicitudes de búsqueda se ejecutan en paralelo - Si el usuario ingresa "a""ab""abc", se ejecutan las 3 solicitudes
- Solicitudes antiguas (resultado de "a") pueden regresar después y sobrescribir el resultado más reciente
Razón de corrección
- Usando
switchMap, cuando comienza una nueva búsqueda, se cancelan las solicitudes antiguas - Siempre se muestra solo el resultado de búsqueda más reciente
Problema 3: Escenario práctico
Escribe código que cumpla los siguientes requisitos.
Puntos clave
- Usuario hace clic en botón
- Obtener en paralelo 3 APIs (información de usuario, lista de posts, lista de comentarios)
- Mostrar datos cuando todo esté completo
- Si ocurre error, devolver datos vacíos
- Cancelar suscripción al destruir componente
Ejemplo de respuesta
import { fromEvent, forkJoin, of, Subject } from 'rxjs';
import { switchMap, catchError, takeUntil } from 'rxjs';
class DataComponent {
private destroy$ = new Subject<void>();
private button = document.querySelector('button')!;
ngOnInit() {
fromEvent(this.button, 'click').pipe(
switchMap(() =>
forkJoin({
user: this.getUserAPI().pipe(
catchError(() => of(null))
),
posts: this.getPostsAPI().pipe(
catchError(() => of([]))
),
comments: this.getCommentsAPI().pipe(
catchError(() => of([]))
)
})
),
takeUntil(this.destroy$)
).subscribe(({ user, posts, comments }) => {
console.log('Obtención de datos completada', { user, posts, comments });
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
private getUserAPI() { /* ... */ }
private getPostsAPI() { /* ... */ }
private getCommentsAPI() { /* ... */ }
}Puntos
forkJoinejecuta 3 APIs en paralelo y espera finalización completa- Establecer valor de fallback en caso de error con
catchErroren cada API switchMapcambia a nueva solicitud con cada clic del botón- Cancelación automática al destruir componente con
takeUntil