NgRx Effects: Choosing switchMap for Reads and concatMap for Writes
NgRx Effects isolate async side effects from components. Learn when to use switchMap for reads versus concatMap for writes, and how to keep the action stream alive with proper error handling.
04 Jun 2026, 03:42 UTC

The problem with async logic inside components
When an Angular component needs data, the direct approach is to inject a service, call it in ngOnInit, and assign the response to a local property. That pattern couples the view to transport details, makes cancellation difficult, and scatters error handling across templates. NgRx Effects move this orchestration out of the component and into a dedicated middleware layer that reacts to dispatched actions.
The useful answer: match the operator to the intent
NgRx Effects listen to the actions$ stream, filter for a specific action type, and perform asynchronous work such as HTTP requests. The critical engineering decision is which RxJS flattening operator to use inside the Effect.
- Use
switchMapfor read operations. If the user triggers the same read action again before the first request finishes,switchMapunsubscribes from the previous inner observable and cancels the pending request. - Use
concatMapfor write operations. It queues inner observables and runs them one at a time in the order received, preventing race conditions where two writes overlap and corrupt state.
In both cases, wrap the inner observable with catchError and map the error to a failure action. Without it, an unhandled exception completes the outer stream and the Effect stops responding to future actions permanently.
How an Effect turns intent into state changes
An Effect is an injectable service that exposes properties typed as Observable<Action>. The actions$ stream emits every action dispatched in the application. The ofType operator narrows that stream to a single action creator. A flattening operator then projects the action into an inner observable—typically an HTTP call from a service. When that inner observable emits, the result is mapped into a new success or failure action, which flows through the normal reducer pipeline.
This decoupling means components only dispatch intentions. They do not subscribe to services, manage loading flags manually, or decide what happens after a response arrives.
A worked ProductsEffects example
The following class assumes NgRx v8 or later, where createEffect is the standard factory. If your package.json shows an older version, verify the API before applying this syntax.
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, concatMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ProductService } from './product.service';
import * as ProductActions from './product.actions';
@Injectable()
export class ProductEffects {
loadProducts$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.loadProducts),
switchMap(() =>
this.productService.getProducts().pipe(
map(products => ProductActions.loadProductsSuccess({ products })),
catchError(error => of(ProductActions.loadProductsFailure({ error })))
)
)
)
);
createProduct$ = createEffect(() =>
this.actions$.pipe(
ofType(ProductActions.createProduct),
concatMap(action =>
this.productService.createProduct(action.product).pipe(
map(product => ProductActions.createProductSuccess({ product })),
catchError(error => of(ProductActions.createProductFailure({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private productService: ProductService
) {}
}
In loadProducts$, switchMap is the correct choice for a read. If a filter change dispatches loadProducts three times in rapid succession, the first two HTTP requests are unsubscribed and only the latest response updates the store. In createProduct$, concatMap ensures that if the user clicks Save twice, the second POST waits for the first to finish. This prevents duplicate entries and keeps the write order deterministic.
Limits and common mistakes
Missing error handling kills the stream
The most common failure is omitting catchError. If getProducts() throws a 500 error and there is no error handler inside the pipe, the outer observable completes. The Effect is dead. Subsequent loadProducts actions are ignored until the application reloads. Always return of(failureAction(...)) from catchError so the stream survives.
Memory leaks from long-lived observables
Finite HTTP observables complete after one emission, so they are generally safe. Problems arise when an Effect subscribes to a long-lived source—such as a WebSocket, a polling interval, or a store selector—without a completion strategy. Use operators like takeUntil paired with a destroy action, or ensure the source completes, to avoid leaking subscriptions.
Boilerplate bloat from overuse
Effects are not a universal hammer. Synchronous state transitions that require no I/O, timers, or browser APIs should go straight to reducers. Routing every toggle through an Effect adds action classes and files without improving testability or correctness.
Version drift
The createEffect syntax shown here applies to NgRx v8 and later. Older applications may use the @Effect() decorator, which has different registration and typing rules. Check your installed version before migrating or copying patterns between projects.
Verifying the behavior
You can confirm the implementation without relying on unit tests alone.
- Redux DevTools: Dispatch
loadProducts. You should observe the trigger action followed by eitherloadProductsSuccessorloadProductsFailure. If only the trigger appears, the service call failed silently or the Effect stream is no longer active. - Network tab: Trigger
loadProductsrepeatedly. WithswitchMap, earlier GET requests should show as canceled. TriggercreateProducttwice quickly. WithconcatMap, the POST requests should appear sequentially rather than overlapping. - Error resilience: Temporarily configure the endpoint to return a server error. Confirm that
loadProductsFailurereaches the store, then restore the endpoint and verify that a newloadProductsaction still triggers the Effect.
These checks validate both the operator choice and the stream's ability to survive failures, which are the two properties most likely to break in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.