NgRx Effects: Decouple Async Logic from Components and Reducers
NgRx Effects let you decouple async logic from components and reducers. Learn how to set up a functional effect, handle errors, and avoid common pitfalls with a concrete example and verification steps.
05 Jun 2026, 22:14 UTC

Why NgRx Effects Matter
When an Angular application grows, mixing HTTP requests, WebSocket streams, or side‑effects directly into components or reducers makes the code hard to test, maintain, and reason about. NgRx Effects give you a clean separation: components dispatch plain actions, reducers stay pure, and all asynchronous work lives in a dedicated, observable‑based layer. The result is a predictable state flow that can be debugged in Redux DevTools and unit‑tested in isolation.
How NgRx Effects Work
Effects are RxJS Observable streams that listen for specific actions dispatched to the Store. When an action matches a filter, the effect runs an asynchronous operation (e.g., an HTTP call) and then emits a new action that will be handled by a reducer or another effect. Because the effect never mutates the state directly, reducers remain synchronous and deterministic.
The core API is createEffect, which takes a function that returns an observable and returns a typed effect. Example:
import { createEffect, ofType, Actions } from '@ngrx/effects';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
@Injectable()
export class DataEffects {
loadData$ = createEffect(() =>
this.actions$.pipe(
ofType(loadData), // listens for the LoadData action
mergeMap(() =>
this.http.get('/api/items').pipe(
map(items => loadDataSuccess({ items })),
catchError(err => of(loadDataFailure({ error: err })))
)
)
)
);
constructor(private actions$: Actions, private http: HttpClient) {}
}
Key points in the snippet:
ofType(loadData)filters the action stream.mergeMapruns the HTTP request and keeps all concurrent requests alive.catchErrorensures the effect stream never terminates on error.- Both success and failure actions are dispatched back to the Store.
Dispatch‑False for Fire‑and‑Forget
Sometimes you need an effect for side‑effects that don’t change state, such as navigation or analytics. Use the dispatch: false option:
navigate$ = createEffect(() =>
this.actions$.pipe(
ofType(loadDataSuccess),
tap(() => this.router.navigate(['/home']))
),
{ dispatch: false }
);
This effect runs when loadDataSuccess fires but never emits a new action.
Functional vs Class‑Based Effects
Recent NgRx releases (v15+) support functional effects, letting you write effects without the @Injectable decorator or a class. This reduces boilerplate and improves testability:
import { createEffect } from '@ngrx/effects';
import { ofType } from '@ngrx/store';
import { map, mergeMap } from 'rxjs/operators';
export const loadData$ = createEffect(({ actions$, http }) =>
actions$.pipe(
ofType(loadData),
mergeMap(() => http.get('/api/items'))
)
);
Common Mistakes and How to Avoid Them
1. Ignoring Errors
Without catchError, a thrown error terminates the observable chain, and the effect stops listening for future actions. Always return an observable, even for failures.
2. Choosing the Wrong Flattening Operator
switchMap cancels previous subscriptions when a new action arrives, which is useful for search-as-you-type but harmful for paginated lists where you want all requests to finish. Use mergeMap for concurrent requests or concatMap for strict order.
3. Over‑Effecting Simple State Changes
Effects should handle side‑effects, not state transformations. If you only need to update a slice of state, dispatch a plain action and let a reducer handle it.
4. Forgetting to Test the Effect Stream
Test effects by providing a mock Actions observable and asserting that the expected output actions are emitted. Tools like provideMockActions from @ngrx/effects/testing simplify this.
Verifying Your Effect Works
- Dispatch
loadDatafrom a component:this.store.dispatch(loadData()); - Open Redux DevTools. You should see:
LOAD_DATALOAD_DATA_SUCCESS(orLOAD_DATA_FAILURE)
- Simulate a network error and ensure
LOAD_DATA_FAILUREis still dispatched and the store continues to accept newLOAD_DATAactions. - For
dispatch: falseeffects, check that the side‑effect (e.g., navigation) occurs without any new action appearing in the dev‑tools timeline.
Limitations to Keep in Mind
- Effects are global; they cannot be scoped to a component unless you use the
EffectsModule.forFeaturepattern. - Because effects run outside Angular’s zone, you must manually trigger change detection if you rely on UI updates that are not driven by the Store.
- Large numbers of effects can increase bundle size; tree‑shakable imports and lazy loading reduce this impact.
Conclusion
NgRx Effects provide a robust, testable way to manage asynchronous logic in Angular. By listening to actions, performing side‑effects, and dispatching new actions, they keep reducers pure and the state flow predictable. Remember to handle errors, choose the right RxJS operator, and test the observable chain to avoid silent failures and unexpected behavior.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.