Cuándo usar los operadores concatMap, mergeMap, switchMap o exhaustMap al construir un CRUD con NgRx

Hace unos días estuve trabajando con NgRx effects en una aplicación donde necesitaba obtener datos del store y combinarlos con llamadas a servicios que involucran tareas asíncronas.
Al trabajar con flujos combinados, es muy importante elegir el operador adecuado según la operación que queremos realizar. Las opciones principales son concatMap, exhaustMap, mergeMap y switchMap.
Como estoy de vacaciones, quise tomarme unos minutos para construir un ejemplo práctico donde tenga que elegir entre cada uno de ellos, comunicarme con una API, manejar tareas asíncronas con effects y actualizar el estado en el reducer.
La idea de este artículo es mantenernos en forma con NgRx y tener claro cuándo y por qué usar cada operador, así que vamos a ello.
Este artículo forma parte de mi serie sobre NgRx. Si quieres seguir el paso a paso completo, puedes revisar las entregas anteriores:
El Proyecto
Quiero crear una sección en mi aplicación para guardar mis lugares favoritos en Menorca. Para esto voy a usar mockAPI.io, un servicio excelente para armar una API de prueba y manejar operaciones CRUD.
Primero, clona el repositorio start-with-ngrx. Este proyecto incluye los ejemplos de mis posts anteriores:
https://github.com/danywalls/start-with-ngrx.gitCambia a la rama crud-ngrx, que ya incluye la siguiente configuración base:
- NgRx instalado y configurado con DevTools.
- Configuración lista para conectar con la API de MockAPI.io.
- El servicio
PlacesServicepara agregar, actualizar, eliminar y obtener lugares. - El componente
PlaceComponent, un componente vacío listo para cargar en la ruta/places.
El Objetivo
El objetivo es practicar lo aprendido en artículos anteriores construyendo un CRUD con NgRx y entendiendo cuándo conviene usar cada operador de RxJS. En este artículo vamos a:
- Crear el estado para los lugares.
- Crear las acciones para las operaciones CRUD.
- Crear los selectores para leer los lugares del estado.
- Usar effects para realizar las acciones de crear, actualizar, eliminar y obtener datos.
- Actualizar y eliminar lugares consumiendo la API.
¡Empecemos!
Si solo te interesa ver la explicación de los operadores, puedes saltar directamente a la sección de implementación de effects.
Crear el Estado de Lugares
Queremos manejar el estado de los lugares, el cual debe guardar lo siguiente:
- Una lista de lugares proveniente de la API.
- El lugar seleccionado para editar o eliminar.
- Un indicador de carga (
loading) mientras se obtienen datos o se ejecuta una acción. - Un mensaje de error si ocurre algún fallo al agregar, actualizar o eliminar.
Con estos puntos claros, creamos un archivo en places/state/places.state.ts, definimos el tipo o interfaz PlacesState (lo que prefieras) y declaramos el estado inicial:
import { Place } from '../../../entities/place.model';
export type PlacesState = {
places: Array<Place>;
placeSelected: Place | undefined;
loading: boolean;
error: string | undefined;
};
export const placesInitialState: PlacesState = {
error: '',
loading: false,
placeSelected: undefined,
places: [],
};¡Perfecto! Ya tenemos el estado inicial de nuestros lugares. Ahora el siguiente paso es definir las acciones que dispararán los cambios.
Crear las Acciones
Igual que en mi post anterior, vamos a usar la función createActionGroup para agrupar las acciones relacionadas con PlaceState. Tenemos dos tipos de acciones: las disparadas por el usuario desde la interfaz y las relacionadas con el proceso de la API. Por eso prefiero dividirlas en dos grupos: PlacesPageActions y PlacesApiActions.
Primero, definimos las acciones de la página PlacePage para lo siguiente:
- Solicitar la carga de lugares:
Load Places. - Agregar o actualizar un lugar: enviar el objeto
Place. - Eliminar un lugar: enviar el identificador del lugar a borrar.
- Seleccionar y deseleccionar un lugar.
El código en places.actions.ts queda de la siguiente manera:
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Place } from '../../../entities/place.model';
export const PlacesPageActions = createActionGroup({
source: 'Places',
events: {
'Load Places': emptyProps(),
'Add Place': props<{ place: Place }>(),
'Update Place': props<{ place: Place }>(),
'Delete Place': props<{ id: string }>(),
'Select Place': props<{ place: Place }>(),
'UnSelect Place': emptyProps(),
},
});Por otro lado, necesitamos acciones para los effects. Estas acciones están vinculadas con la API y gestionan las respuestas exitosas o los fallos al cargar, agregar, actualizar o eliminar registros:
export const PlacesApiActions = createActionGroup({
source: 'PlaceAPI',
events: {
'Load Success': props<{ places: Array<Place> }>(),
'Load Fail': props<{ message: string }>(),
'Add Success': props<{ place: Place }>(),
'Add Failure': props<{ message: string }>(),
'Update Success': props<{ place: Place }>(),
'Update Failure': props<{ message: string }>(),
'Delete Success': props<{ id: string }>(),
'Delete Failure': props<{ message: string }>(),
},
});Con las acciones listas, el siguiente paso lógico es crear el reducer para procesar estos eventos y actualizar nuestro estado.
El Reducer
El reducer se encarga de actualizar el estado en respuesta a las acciones. En algunos casos queremos modificar el estado inmediatamente cuando el usuario dispara una acción, mientras que en otros casos lo actualizamos con el resultado que entrega el effect.
Por ejemplo, la acción loadPlaces se dispara desde el componente PlaceComponent. En ese instante quiero mostrar un indicador de carga, así que pongo loading en true. En otros casos, como en la acción loadSuccess, el evento llega desde el effect una vez que el servicio PlacesService devuelve los datos.
Implementamos cada manejador para actualizar el estado según lo requerido. El código completo del reducer queda así:
import { createReducer, on } from '@ngrx/store';
import { placesInitialState } from './places.state';
import { PlacesApiActions, PlacesPageActions } from './places.actions';
export const placesReducer = createReducer(
placesInitialState,
on(PlacesPageActions.loadPlaces, (state) => ({
...state,
loading: true,
})),
on(PlacesPageActions.selectPlace, (state, { place }) => ({
...state,
placeSelected: place,
})),
on(PlacesPageActions.unSelectPlace, (state) => ({
...state,
placeSelected: undefined,
})),
on(PlacesApiActions.loadSuccess, (state, { places }) => ({
...state,
places: [...places],
})),
on(PlacesApiActions.loadFailure, (state, { message }) => ({
...state,
loading: false,
error: message,
})),
on(PlacesApiActions.addSuccess, (state, { place }) => ({
...state,
loading: false,
places: [...state.places, place],
})),
on(PlacesApiActions.addFailure, (state, { message }) => ({
...state,
loading: false,
message,
})),
on(PlacesApiActions.updateSuccess, (state, { place }) => ({
...state,
loading: false,
placeSelected: undefined,
places: state.places.map((p) => (p.id === place.id ? place : p)),
})),
on(PlacesApiActions.updateFailure, (state, { message }) => ({
...state,
loading: false,
message,
})),
on(PlacesApiActions.deleteSuccess, (state, { id }) => ({
...state,
loading: false,
placeSelected: undefined,
places: [...state.places.filter((p) => p.id !== id)],
})),
on(PlacesApiActions.deleteFailure, (state, { message }) => ({
...state,
loading: false,
message,
})),
);Ya tenemos el reducer listo para reaccionar a cada acción. Ahora entramos en la parte central del artículo: los effects y la elección de los operadores adecuados.
El Effect
Un effect escucha acciones, ejecuta tareas asíncronas y despacha otras acciones. Es fundamental saber qué operador de RxJS usar (concatMap, exhaustMap, mergeMap o switchMap) para no provocar condiciones de carrera en tu código.
¿Qué es una condición de carrera (Race Condition)? Una condición de carrera ocurre cuando varias tareas asíncronas (observables) se ejecutan al mismo tiempo e interfieren entre sí de forma no deseada. Esto produce comportamientos inesperados o errores porque el orden de finalización de esas tareas no está bajo control.
Por este motivo, conviene conocer bien las características de cada operador:
- exhaustMap: Ignora nuevas peticiones si ya hay una en curso. ⚠️ Puede generar problemas si las peticiones ignoradas eran importantes, ya que se descartan por completo y nunca se ejecutan.
- mergeMap: Permite que todas las peticiones se ejecuten al mismo tiempo, pero debes gestionar bien las respuestas para evitar problemas. ⚠️ Puede provocar condiciones de carrera porque varias peticiones se procesan en paralelo, lo que puede devolver resultados desordenados o modificar recursos compartidos simultáneamente.
- concatMap: Toma cada petición una tras otra, en estricto orden. Es el operador más seguro frente a condiciones de carrera, ya que las peticiones se encolan y se procesan de forma secuencial. ✅
- switchMap: Cancela la petición anterior en cuanto entra una nueva. Esto garantiza que solo se procese la última petición. ✅ No genera condiciones de carrera porque solo hay una petición activa en cada momento; las anteriores se cancelan y no afectan el resultado actual.
Veamos cómo aplicar cada uno de ellos en nuestros effects.
Si quieres aprender más sobre estos operadores con explicaciones visuales, te recomiendo mucho ver los videos de @decodedfrontend.
ExhaustMap
exhaustMap ignora cualquier nueva petición si ya hay una en curso. Resulta muy útil en casos donde solo debe procesarse la primera solicitud y descartar las siguientes hasta que termine la activa. Un buen ejemplo de esto es obtener la lista inicial de lugares:
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { inject } from '@angular/core';
import { PlacesService } from '../../../services/places.service';
import { PlacesApiActions, PlacesPageActions } from './places.actions';
import { catchError, exhaustMap, map, of } from 'rxjs';
export const loadPlacesEffect = createEffect(
(actions$ = inject(Actions), placesService = inject(PlacesService)) => {
return actions$.pipe(
ofType(PlacesPageActions.loadPlaces),
exhaustMap(() =>
placesService.getAll().pipe(
map((places) => PlacesApiActions.loadSuccess({ places })),
catchError((error) =>
of(PlacesApiActions.loadFailure({ message: error })),
),
),
),
);
},
{ functional: true },
);Si el usuario solicita cargar los lugares varias veces mientras la llamada HTTP sigue pendiente, exhaustMap ignora los intentos repetidos y evita llamadas innecesarias al servidor. Ahora veamos qué pasa cuando sí necesitamos procesar cada petición en orden estricto.
ConcatMap
concatMap nos ayuda a mapear acciones y combinar nuestros observables en un único flujo de forma ordenada, esperando a que cada uno termine antes de continuar con el siguiente. Es la opción más segura cuando necesitamos garantizar que las operaciones se ejecuten en el orden exacto en que fueron disparadas. Por ejemplo, al actualizar un lugar queremos que la primera actualización termine antes de procesar una nueva:
export const updatePlaceEffect$ = createEffect(
(actions$ = inject(Actions), placesService = inject(PlacesService)) => {
return actions$.pipe(
ofType(PlacesPageActions.addPlace),
concatMap(({ place }) =>
placesService.update(place).pipe(
map((apiPlace) =>
PlacesApiActions.updateSuccess({ place: apiPlace }),
),
catchError((error) =>
of(PlacesApiActions.updateFailure({ message: error })),
),
),
),
);
},
{ functional: true },
);Al asegurar que una petición no empiece hasta que la anterior haya finalizado, evitamos colisiones al actualizar datos. Sin embargo, hay escenarios donde no necesitamos esperar y preferimos velocidad; ahí es donde entra mergeMap.
MergeMap
El operador mergeMap es rápido porque no mantiene un orden de espera. Permite que todas las peticiones se ejecuten de forma concurrente, aunque requiere prestar atención a las respuestas para evitar condiciones de carrera. En este escenario funciona bien para agregar o eliminar elementos independientes.
El Effect para Agregar Lugares
export const addPlacesEffect$ = createEffect(
(actions$ = inject(Actions), placesService = inject(PlacesService)) => {
return actions$.pipe(
ofType(PlacesPageActions.addPlace),
mergeMap(({ place }) =>
placesService.add(place).pipe(
map((apiPlace) => PlacesApiActions.addSuccess({ place: apiPlace })),
catchError((error) =>
of(PlacesApiActions.addFailure({ message: error })),
),
),
),
);
},
{ functional: true },
);El Effect para Eliminar Lugares
Para la acción de eliminar preparé dos effects. El primero llama al servicio y despacha deleteSuccess para actualizar el estado:
export const deletePlaceEffect$ = createEffect(
(actions$ = inject(Actions), placesService = inject(PlacesService)) => {
return actions$.pipe(
ofType(PlacesPageActions.deletePlace),
mergeMap(({ id }) =>
placesService.delete(id).pipe(
map((id_response) =>
PlacesApiActions.deleteSuccess({ id: id_response }),
),
catchError((error) =>
of(PlacesApiActions.deleteFailure({ message: error })),
),
),
),
);
},
{ functional: true },
);Además, cuando el usuario elimina un elemento quiero refrescar los datos desde el servidor. Por eso agrego un segundo effect que escucha deleteSuccess y vuelve a pedir la lista de lugares para sincronizar el estado:
export const deletePlaceSuccessEffect$ = createEffect(
(actions$ = inject(Actions), placesService = inject(PlacesService)) => {
return actions$.pipe(
ofType(PlacesApiActions.deleteSuccess),
mergeMap(() =>
placesService.getAll().pipe(
map((places) => PlacesApiActions.loadSuccess({ places })),
catchError((error) =>
of(PlacesApiActions.loadFailure({ message: error.message })),
),
),
),
);
},
{ functional: true },
);Con mergeMap cubrimos las peticiones paralelas, pero falta hablar de uno de los operadores más utilizados en Angular cuando solo nos importa la respuesta más reciente.
¿Y qué pasa con SwitchMap?
Usa switchMap cuando quieras asegurarte de que solo se procese la última petición emitida y se cancelen las anteriores que sigan en curso. Es ideal para casos como autocompletado o campos de búsqueda en vivo. En este CRUD en particular no lo necesité directamente, pero es indispensable tenerlo en cuenta porque resuelve de raíz los problemas de peticiones obsoletas en búsquedas.
Una vez que tenemos definidos los effects y el reducer, el siguiente paso es registrarlos en la configuración de la aplicación.
Registrar Effects y Reducer
Llegó el momento de registrar nuestro nuevo estado placesReducer y los placesEffects. Abrimos el archivo app.config.ts:
- Agregamos una nueva clave en
provideStore:places: placesReducer. - Importamos todos los effects desde
places.effects.tsy los pasamos a la funciónprovideEffects.
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideStore } from '@ngrx/store';
import { homeReducer } from './pages/home/state/home.reducer';
import { placesReducer } from './pages/places/state/places.reducer';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authorizationInterceptor } from './interceptors/authorization.interceptor';
import { provideEffects } from '@ngrx/effects';
import * as homeEffects from './pages/home/state/home.effects';
import * as placesEffects from './pages/places/state/places.effects';
export const appConfig = {
providers: [
provideRouter(routes),
provideStore({
home: homeReducer,
places: placesReducer, // registrar placesReducer
}),
provideStoreDevtools({
name: 'nba-app',
maxAge: 30,
trace: true,
connectInZone: true,
}),
provideEffects([homeEffects, placesEffects]), // registrar placesEffects
provideAnimationsAsync(),
provideHttpClient(withInterceptors([authorizationInterceptor])),
],
};Ya tenemos registrados los effects y el reducer, pero ¿cómo leemos estos datos dentro de los componentes? Para eso necesitamos crear selectores.
Selectores
Creamos el archivo places.selector.ts. Con la función createFeatureSelector apuntamos al tipo PlacesState y definimos el nombre de la feature:
const selectPlaceState = createFeatureSelector<PlacesState>('places');A partir de selectPlaceState, usamos la función createSelector para obtener partes específicas de nuestro estado como places, placeSelected, loading y error. Luego exportamos estos selectores para consumirlos directamente en los componentes:
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { PlacesState } from './places.state';
const selectPlaceState = createFeatureSelector<PlacesState>('places');
const selectPlaces = createSelector(
selectPlaceState,
(placeState) => placeState.places,
);
const selectPlaceSelected = createSelector(
selectPlaceState,
(placeState) => placeState.placeSelected,
);
const selectLoading = createSelector(
selectPlaceState,
(placeState) => placeState.loading,
);
const selectError = createSelector(
selectPlaceState,
(placeState) => placeState.error,
);
export default {
placesSelector: selectPlaces,
selectPlaceSelected: selectPlaceSelected,
loadingSelector: selectLoading,
errorSelector: selectError,
};Con los selectores en su lugar, ahora podemos conectar la interfaz de usuario con el store.
Usar Acciones y Selectores
El paso final es consumir los selectores y despachar acciones desde los componentes. Abrimos places.component.ts e inyectamos el store. Luego declaramos places$, error$ y placeSelected$ para obtener los valores emitidos por los selectores:
store = inject(Store);
places$ = this.store.select(PlacesSelectors.placesSelector);
error$ = this.store.select(PlacesSelectors.errorSelector);
placeSelected$ = this.store.select(PlacesSelectors.selectPlaceSelected);Para mostrar los lugares creé dos componentes auxiliares: place-card y place-form.
- place-card: Muestra la información del lugar y permite seleccionarlo o eliminarlo.
- place-form: Permite cambiar el nombre del lugar seleccionado.
¡Vamos a implementarlos!
El Componente PlaceCard
Recibe el lugar mediante una propiedad input y despacha dos acciones: seleccionar y eliminar.
import { Component, inject, input } from '@angular/core';
import { Place } from '../../entities/place.model';
import { Store } from '@ngrx/store';
import { PlacesPageActions } from '../../pages/places/state/places.actions';
@Component({
selector: 'app-place-card',
standalone: true,
imports: [],
templateUrl: './place-card.component.html',
styleUrl: './place-card.component.scss',
})
export class PlaceCardComponent {
place = input.required<Place>();
store = inject(Store);
edit() {
this.store.dispatch(PlacesPageActions.selectPlace({ place: this.place() }));
}
remove() {
this.store.dispatch(PlacesPageActions.deletePlace({ id: this.place().id }));
}
}En la plantilla vinculamos el input del lugar y agregamos dos botones para llamar a los métodos edit y remove:
<div class="column">
<div class="card place-card">
<div class="card-image">
<figure class="image is-4by3">
<img [alt]="place().description" [src]="place().avatar">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<p class="title is-4">{{ place().name }}</p>
<p class="subtitle is-6">Visited on: <span>{{ place().createdAt }}</span></p>
</div>
</div>
<div class="content">
{{ place().description }}.
<br>
<strong>Price:</strong> {{ place().price }}
</div>
<div class="buttons">
<button (click)="edit()" class="button is-info">Change Name</button>
<button (click)="remove()" class="button is-danger">Remove</button>
</div>
</div>
</div>
</div>Ahora pasemos a place-form. Este componente es un poco diferente porque toma selectedPlace directamente del estado y despacha la acción de actualización:
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { FormsModule } from '@angular/forms';
import { AsyncPipe, JsonPipe } from '@angular/common';
import PlacesSelectors from '../../pages/places/state/places.selectors';
import { PlacesPageActions } from '../../pages/places/state/places.actions';
import { Place } from '../../entities/place.model';
@Component({
selector: 'app-place-form',
standalone: true,
imports: [FormsModule, JsonPipe, AsyncPipe],
templateUrl: './place-form.component.html',
styleUrl: './place-form.component.scss',
})
export class PlaceFormComponent {
store = inject(Store);
placeSelected$ = this.store.select(PlacesSelectors.selectPlaceSelected);
delete(id: string) {
this.store.dispatch(PlacesPageActions.deletePlace({ id }));
}
save(place: Place, name: string) {
this.store.dispatch(
PlacesPageActions.updatePlace({
place: {
...place,
name,
},
}),
);
}
}En la plantilla nos suscribimos a placeSelected$ y, para mantener el ejemplo simple y el artículo corto, vinculamos selectedPlace.name usando una variable de plantilla #placeName.
Al hacer clic en el botón Guardar, pasamos el selectedPlace y el nuevo valor del nombre:
Sé que se puede estructurar de forma más limpia, pero no quería alargar demasiado el artículo. Si se te ocurre una mejor solución, deja un comentario o envía un PR en GitHub y con gusto mencionaré tu aporte.
@if (placeSelected$ | async; as selectedPlace) {
<div class="field">
<label class="label" for="placeName">Place Name</label>
<div class="control">
<input id="placeName" #placeName [value]="selectedPlace.name" class="input" placeholder="Place Name" type="text">
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button (click)="save(selectedPlace, placeName.value)" class="button is-primary">Save</button>
</div>
<div class="control">
<button (click)="delete(selectedPlace.id)" class="button is-danger">Delete</button>
</div>
</div>
}Por último, en places.component integramos place-card y place-form junto con los selectores del store.
Aquí hacemos tres cosas clave:
- Nos suscribimos a
error$para mostrar un aviso si ocurre un fallo. - Nos suscribimos a
places$y usamos@forpara renderizar la lista de tarjetas. - Nos suscribimos a
placeSelected$para abrir una ventana modal cuando el usuario seleccione un lugar y llamar aonClose()al cerrar.
El código final de la plantilla queda así:
@if (error$ | async; as error) {
<h3>Ups we found a error {{ error }}</h3>
}
@if (places$ | async; as places) {
<div class="columns is-multiline is-flex is-flex-wrap-wrap">
@for (place of places; track place) {
<app-place-card [place]="place"/>
}
</div>
}
@if (placeSelected$ | async; as selectedPlace) {
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content">
<app-place-form></app-place-form>
</div>
<button (click)="onClose()" class="modal-close is-large" aria-label="close"></button>
</div>
}Conclusión
En este artículo aprendimos a crear una aplicación CRUD con NgRx y a usar diferentes operadores de RxJS para manejar acciones asíncronas. También practicamos la gestión de peticiones y respuestas HTTP conectándonos a una API. Sin embargo, siento que hay varios puntos que podemos mejorar.
Puntos a mejorar:
- Evitar despachar acciones manualmente solo por navegar a la ruta del componente
/places. - Reducir la cantidad excesiva de código repetitivo que exige una operación CRUD básica.
Para resolver estos puntos, tengo planeado refactorizar el proyecto combinando NgRx Entity con NgRx Router Store para simplificar el código y facilitar el mantenimiento.
Recursos:
Preguntas Frecuentes
¿Cuándo debería usar switchMap frente a mergeMap en un efecto de NgRx?
Usa switchMap cuando quieras cancelar una petición anterior en curso si se despacha una nueva acción antes de que termine la primera. Es ideal para búsquedas o cargas de datos donde solo importa la última respuesta. Usa mergeMap cuando quieras que todas las peticiones terminen de forma independiente sin cancelarse, algo adecuado para operaciones como eliminar elementos donde cada petición debe completarse.
¿Cuál es la diferencia entre concatMap y exhaustMap en RxJS?
Ambos operadores evitan que las peticiones se superpongan, pero lo hacen de forma contraria. concatMap encola cada observable entrante y los procesa uno tras otro en orden, sin perder ninguna petición. exhaustMap ignora nuevos observables mientras haya uno activo, lo cual es muy útil para botones de inicio de sesión o envío de formularios donde quieres bloquear clics duplicados hasta que termine la primera petición.
¿Qué operador de aplanamiento de RxJS debería usar para crear o actualizar en efectos de NgRx?
Para operaciones de creación y actualización, concatMap suele ser la opción más segura porque procesa cada petición en el orden en que fue despachada y no descarta ninguna acción. Esto mantiene la secuencia de operaciones, algo clave cuando el resultado de una escritura depende de que la anterior haya terminado correctamente.
¿Por qué debería evitar usar mergeMap para una acción de eliminación en NgRx si el orden importa?
mergeMap ejecuta todos los observables internos de forma concurrente sin garantizar ningún orden. Si despachas varias acciones de eliminación seguidas, las respuestas de la API pueden llegar desordenadas y causar inconsistencias en el estado. Para operaciones destructivas donde el orden importa, conviene usar concatMap para asegurar que cada eliminación termine antes de que empiece la siguiente.
¿Cuál es un buen caso de uso real para exhaustMap en Angular?
exhaustMap funciona muy bien para gestionar envíos de formularios o peticiones de autenticación donde quieres evitar que el usuario dispare una segunda llamada a la API mientras la primera sigue pendiente. Al ignorar las acciones siguientes hasta que termine el observable activo, evitas peticiones duplicadas y posibles condiciones de carrera sin depender únicamente de deshabilitar el botón en la vista.
Artículos Relacionados
Cómo manejar efectos secundarios en Angular usando NgRx Effects
Los efectos secundarios son habituales en nuestras aplicaciones. Aprende a manejarlos en Angular con NgRx Effects para evitar sobrecargar tus componentes y mantener tu estado limpio y predecible.
Cómo usar NgRx Selectors en Angular
En NgRx, cuando queremos obtener datos del store, la forma más rápida suele ser usar store.select, pero tiene riesgos de tipado. Aprende a usar Selectors para consultar el estado con tipado seguro y mejor rendimiento.
Parámetros de URL en Angular Router usando NgRx Router Store
Al crear aplicaciones con estado, a veces necesitamos conservarlo en la URL para que los usuarios puedan guardar o compartir enlaces específicos, mejorando la navegación con NgRx Router Store.
Cómo probar promesas con Mocha.
Las promesas son muy comunes en JavaScript para ejecutar código asíncrono, pero ¿cómo podemos probarlas? En este artículo te muestro cómo crear un método que retorna una promesa y escribir pruebas tanto para cuando se resuelve como cuando es rechazada.
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.