Managing Asynchronous API Calls with NgRx Effects
Learn how to implement NgRx Effects to handle asynchronous API calls, prevent race conditions with RxJS flattening operators, and avoid common stream termination errors.
15 Dec 2025, 02:32 UTC

The Problem: Blocking the UI Thread with Side Effects
In a Redux-based architecture, reducers must be pure functions. This means they cannot perform asynchronous operations, such as HTTP requests, because they must return the new state immediately and predictably. If you trigger an API call directly inside a component or a reducer, you risk creating unpredictable state transitions, race conditions, and a UI that freezes while waiting for a server response.
The solution is NgRx Effects. Effects provide a dedicated layer to handle "side effects"—tasks that interact with the outside world—keeping your components lean and your state transitions predictable.
Prerequisites
- An Angular project with
@ngrx/storeand@ngrx/effectsinstalled. - A defined set of Actions (e.g.,
loadUsers,loadUsersSuccess,loadUsersFailure). - An Angular Service that handles the actual
HttpClientcalls.
Implementing the Effect Pattern
To manage an asynchronous flow, you must implement a three-action cycle: a trigger action, a success action, and a failure action. This ensures the state reflects the current status of the request (Loading, Loaded, or Error).
1. Define the Effect Logic
Create an effect class and use the createEffect function. This function tells NgRx that the resulting observable should have its emissions dispatched back to the store.
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, exhaustMap, catchError } from 'rxjs/operators';
import { UserService } from './user.service';
import * as UserActions from './user.actions';
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
exhaustMap(() =>
this.userService.getAll().pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error => of(UserActions.loadUsersFailure({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private userService: UserService
) {}
}
2. Choosing the Right Flattening Operator
The choice of RxJS flattening operator determines how the effect handles concurrent requests. Using the wrong one can lead to data corruption or wasted bandwidth.
| Operator | Behavior | Best Use Case |
|---|---|---|
switchMap |
Cancels the previous request if a new action arrives. | Search-as-you-type or filtering. |
exhaustMap |
Ignores new actions until the current request completes. | Login buttons or "Save" submissions. |
mergeMap |
Runs all requests concurrently. | Deleting multiple independent items. |
concatMap |
Queues requests to run one after another. | Sequential updates where order matters. |
3. Registering the Effect
Effects must be registered in your application configuration to be active. In a modular application, add them to the EffectsModule.
// In app.module.ts or a feature module
import { EffectsModule } from '@ngrx/effects';
import { UserEffects } from './state/user.effects';
@NgModule({
imports: [
EffectsModule.forRoot([UserEffects]) // Use .forFeature([UserEffects]) for lazy-loaded modules
]
})
export class AppModule {}
Critical Engineering Considerations
The Stream Termination Risk
A common failure point in NgRx Effects is placing catchError in the wrong location. If catchError is placed on the outer pipe (the one listening to actions$), an API error will kill the entire effect stream. Once the stream terminates, the effect will stop listening to actions until the application is refreshed.
Correct Approach: Always place catchError inside the inner observable (the API call pipe). This ensures that only the inner stream fails, while the outer actions$ stream remains active.
Verification and Diagnostics
- Redux DevTools: Open the browser extension. Dispatch
loadUsers. You should seeloadUsersimmediately, followed by a delay, and then eitherloadUsersSuccessorloadUsersFailure. - Network Tab: If using
exhaustMap, click a "Load" button rapidly five times. Verify that only one network request is initiated. - State Inspection: Ensure the reducer handles the failure action to clear "loading" flags, otherwise the UI may show a permanent spinner.
Rollback and Cleanup
Because Effects are services managed by the Angular dependency injector, they are cleaned up when the module they belong to is destroyed. If you are using EffectsModule.forFeature() in a lazy-loaded module, the effect will be automatically removed from memory when the user navigates away from that feature area.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.