Choosing the Right RxJS Flattening Operator for NgRx Effects
Avoid race conditions in NgRx Effects by choosing the right RxJS flattening operator. Learn when to use switchMap, exhaustMap, concatMap, and mergeMap for API calls.
14 Sept 2026, 03:26 UTC

The Problem: Unpredictable API Race Conditions
In a standard Angular application, moving API calls from components into NgRx Effects centralizes logic, but it introduces a new risk: race conditions. When a user clicks a button three times rapidly, or types into a search bar, the application may trigger multiple concurrent HTTP requests. If these requests return out of order, your state might reflect an older request that finished last, leading to inconsistent UI data.
The solution is not just using Effects, but selecting the correct flattening operator. These operators determine how the Effect handles a new incoming action while a previous asynchronous task is still pending.
Mapping the Operator to the Use Case
Choosing an operator depends entirely on whether you want to cancel, ignore, queue, or parallelize the incoming requests.
switchMap: The 'Latest Only' Approach
switchMap cancels the previous inner observable as soon as a new action arrives. This is essential for search-as-you-type functionality. If the user types "Angu" and then "Angular", the request for "Angu" is no longer relevant and should be aborted to save bandwidth and prevent the wrong result from populating the store.
exhaustMap: The 'Ignore Until Finished' Approach
exhaustMap ignores all incoming actions until the current observable completes. This is the primary defense against "double-submit" bugs. For login forms or payment processing, you want to ensure that only one request is active, regardless of how many times the user clicks the submit button.
concatMap: The 'Strict Sequence' Approach
concatMap queues incoming actions and processes them one by one in the order they were dispatched. Use this when the order of operations is critical—for example, a series of database updates where step B depends on the successful completion of step A.
mergeMap: The 'Parallel' Approach
mergeMap handles all requests concurrently. It does not cancel, ignore, or queue. This is appropriate for independent actions, such as deleting multiple items from a list where the success of one deletion does not affect the others.
Implementation Example: Search vs. Save
Below is a configuration for a User Profile module. Note how the search functionality uses switchMap to maintain freshness, while the save functionality uses exhaustMap to prevent duplicate entries.
import { Injectable } from '@angular';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, switchMap, exhaustMap, catchError }
import { UserService } from './user.service';
import * as UserActions from './user.actions';
@Injectable()
export class UserEffects {
// Search: Cancel previous requests if a new search term is entered
searchUser$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.searchUser),
switchMap(({ query }) => this.userService.search(query).pipe(
map(results => UserActions.searchSuccess({ results })),
catchError(error => of(UserActions.searchFailure({ error })))
))
));
// Save: Ignore clicks until the current save operation completes
saveUser$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.saveUser),
exhaustMap((user) => this.userService.update(user).pipe(
map(() => UserActions.saveSuccess()),
catchError(error => of(UserActions.saveFailure({ error })))
))
));
constructor(private actions$: Actions, private userService: UserService) {}
}
Verification and Risks
To verify these behaviors, use the Browser Network Tab. For switchMap, you should see the status of previous requests change to "(canceled)" when a new request starts. For exhaustMap, rapid clicks should result in only one network request being sent.
Critical Risk: Always include a catchError inside the inner observable (the one inside the mapping operator). If an error reaches the top-level Effect stream, the observable completes, and the Effect will stop listening to all future actions until the application is reloaded.
Trade-offs and Limitations
While these operators solve race conditions, they can introduce perceived latency. For instance, concatMap can create a bottleneck if one request in a long queue is slow, delaying all subsequent updates. Similarly, exhaustMap can frustrate users if they make a legitimate change and click "Save" again, only to have the action silently ignored because the first request is still hanging.
Actionable Summary
When auditing your NgRx Effects, apply these rules of thumb:
- Search/Filtering? Use
switchMap. - Login/Submit/Save? Use
exhaustMap. - Ordered Sequence? Use
concatMap. - Independent/Parallel? Use
mergeMap.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.