Choosing the Right RxJS Mapping Operator in NgRx Effects
Stop race conditions in Angular. Learn when to use switchMap, exhaustMap, concatMap, and mergeMap in NgRx Effects to handle API calls and user inputs correctly.
25 Dec 2025, 22:52 UTC

The Race Condition Problem in State Management
In a complex Angular application, components should only be responsible for updating the UI and dispatching actions. The heavy lifting—API calls, timers, and external integrations—happens in NgRx Effects. However, a common engineering failure occurs when developers use the same mapping operator for every asynchronous call. This leads to "race conditions," where an older API response arrives after a newer one, overwriting the state with stale data, or where a user accidentally triggers five identical POST requests by double-clicking a submit button.
The takeaway: The choice of RxJS flattening operator (switchMap, mergeMap, concatMap, or exhaustMap) determines how your application handles concurrency and determines whether your data remains consistent.
When to Cancel: switchMap for Read-Only Queries
switchMap is the primary tool for "search-as-you-type" or tab-switching logic. When a new action is dispatched, switchMap immediately cancels the previous inner observable if it hasn't completed yet. This prevents the UI from flickering between old and new results if a slower request finishes after a faster, more recent one.
Risk: Never use switchMap for write operations (POST, PUT, DELETE). If a user triggers a save action and then triggers another action that causes the effect to switch, the first HTTP request might be cancelled by the browser before the server processes it, leading to unpredictable data states.
When to Queue: concatMap for Sequential Updates
If the order of execution is critical—such as a series of database updates where Step B depends on the completion of Step A—concatMap is the correct choice. It queues incoming actions and processes them one by one, ensuring that the second request does not start until the first one completes.
When to Ignore: exhaustMap for Form Submissions
exhaustMap is designed for "non-reentrant" operations. It ignores all incoming actions until the current observable completes. This is the ideal pattern for login buttons or payment submissions. If a user clicks "Submit" three times in one second, exhaustMap handles the first click and ignores the subsequent two until the server responds.
When to Parallelize: mergeMap for Independent Tasks
When you have multiple independent requests that don't depend on each other and don't need to be ordered (e.g., deleting multiple items from a list individually), mergeMap allows them to run in parallel. This maximizes throughput but offers no control over which request finishes first.
Practical Implementation Example
Assume an application with a search feature and a save feature. The following Effect implementation demonstrates the distinction between switchMap and exhaustMap. This code should be placed in an @Injectable class decorated with @Injectable() and provided in the app root.
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { catchError, switchMap, exhaustMap, map }
import { ApiService } from './api.service';
import * as UserActions from './user.actions';
@Injectable()
export class UserEffects {
// SEARCH: Cancel previous requests to avoid stale data
searchUser$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.searchUser),
switchMap(({ query }) => this.apiService.search(query).pipe(
map(results => UserActions.searchSuccess({ results })),
catchError(error => of(UserActions.searchFailure({ error })))
))
));
// SAVE: Ignore new clicks until the current save is finished
saveProfile$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.saveProfile),
exhaustMap(({ profile }) => this.apiService.updateProfile(profile).pipe(
map(() => UserActions.saveSuccess()),
catchError(error => of(UserActions.saveFailure({ error })))
))
));
constructor(private actions$: Actions, private apiService: ApiService) {}
}
Verification and Diagnostics
To verify these behaviors, use the Redux DevTools browser extension and the Network Tab in Chrome/Firefox DevTools:
- For
switchMap: Type rapidly into your search box. You should see multiplesearchUseractions in DevTools, but the Network tab should show previous requests being "Canceled" as new ones start. - For
exhaustMap: Click a submit button rapidly. You should see only one network request triggered, regardless of how many times the button was pressed, until the first request returns a response.
Trade-offs and Limitations
While these operators solve concurrency, they can introduce "boilerplate fatigue." If every single action requires a dedicated Effect with a Success and Failure action, the codebase grows rapidly. For simple state changes that don't require asynchronous logic, avoid Effects entirely and handle the logic within a Reducer. Additionally, be wary of circular dependencies: if an Effect dispatches an action that the same Effect listens for without a filter, you will create an infinite loop that crashes the browser tab.
Actionable Summary
Audit your current NgRx Effects. If you are using mergeMap or switchMap for everything, identify your write operations and move them to exhaustMap (for buttons) or concatMap (for ordered sequences) to prevent data corruption and duplicate server entries.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.