Original cover photo by Josh Redd on Unsplash.
NgRx Effects serve as one of the more widely adopted pieces of the NgRx library, a state management solution designed for Angular. Typically, effects handle asynchronous operations that exist outside of the application's core state, such as HTTP requests, WebSocket connections, writing to browser storage, and similar activities.
The majority of effects follow a fairly simple pattern: they listen for a specific action, execute a side effect, and then produce an Observable stream that emits a new action signaling the completion of that operation. NgRx effects automatically forward that resulting action to the reducer layer, triggering a state transition.
However, certain situations call for incorporating state-aware logic directly within the Effects layer, and that's where RxJS operators become exceptionally valuable. Let's go through a few practical examples.
Blocking an Effect based on Store data
There are instances where an Effect should remain inactive — for example, if the current user lacks certain permissions or if other Store-related state dictates otherwise. Since the Store itself is exposed as an Observable, its state can be leveraged for real-time checks inside an Effect. Here’s a concrete illustration:
loadData$ = createEffect(() => this.actions$.pipe(
ofType(loadData),
withLatestFrom(this.store.select(permissions)))),
filter(([action, permissions]) => hasSpecificPermission(permissions)),
map(([action]) => action),
mergeMap(({payload}) => this.dataService.getData(payload).pipe(
map(data => loadDataSuccess(data)),
catchError(error => of(loadDataError(error))),
))
));
In the code above, a certain permission needs to be stored in the application state before any HTTP call can proceed. This is handled through a sequence of three operators:
withLatestFrompulls the current permission value from the Store via a selectorfilterprevents execution when that permission is missingmapextracts just the action, since the permission value is no longer needed downstream
To avoid duplicating this chain of operators across multiple effects, it's a good idea to encapsulate it into a custom operator. That operator should:
- Take a boolean Observable that indicates whether the effect should continue or stop
- Apply that value to filter out emissions
- Transform the stream back to just the original action
One way to build that custom operator is shown here:
function allowWhen(decider$: Observable<boolean>) {
return function<T>(source$: Observable<T>): Observable<T> {
return source$.pipe(
withLatestFrom(decider$),
filter(([value, decider]) => decider),
map(([value]) => value),
)
}
}
Now we can plug this operator directly into any effect's pipeline:
loadData$ = createEffect(() => this.actions$.pipe(
ofType(loadData),
allowWhen(
this.store.select(permissions).pipe(
map(permissions => hasSpecificPermission(permissions)),
)
),
mergeMap(({payload}) => this.dataService.getData(payload).pipe(
map(data => loadDataSuccess(data)),
catchError(error => of(loadDataError(error))),
))
));
Ensuring required data is available before running an effect
Another common scenario occurs when a component dispatches an action, but the effect responsible for handling it depends on some other piece of state that hasn't yet been loaded into the Store. In such cases, the effect should delay its execution until the necessary data finally arrives, and only then perform the side effect.
One might initially consider skipUntil for this task, but that operator has a fundamental limitation. After the notifier Observable emits once, it permanently opens the floodgates — however, the source itself must emit a new value for anything to flow through. So in practice, you'd still be waiting for another dispatched action before your effect can proceed, which defeats the purpose.
What we actually need is a pattern that sounds like: "wait for the other Observable to emit, then pass through the current emission", rather than "ignore the current emission until another Observable has emitted at some point in the past". The solution looks like this:
loadData$ = createEffect(() => this.actions$.pipe(
ofType(loadData),
switchMap(() => this.store.select(otherData).pipe(
skipWhile(data => data === null),
mergeMap(() => this.dataService.getData().pipe(
map(payload => loadDataSuccess({ payload })),
catchError(error => of(loadDataError({ error }))),
)),
))
));
Here, instead of processing the action immediately, we switch to a selector that reads from the Store and verifies that the required data is present. If that data is missing, the stream stays idle. As soon as the Store updates and the data becomes available, the original action flows through the pipeline and the side effect is finally executed.
Dealing with a side effect that leaves the Store untouched
Sometimes the desired outcome of an effect is not a state change but a purely external operation. For instance, we may need to reroute the user to a different page in response to an action, without altering any state in the Store. In that case, we don't want to emit an action at the end of the effect. This can be done using the tap operator along with a special configuration passed to createEffect:
redirectAfterDataSaved$ = createEffect(() => this.actions$.pipe(
ofType(dataSavedSuccess),
tap(({payload}) => this.router.navigateByUrl(`some-page/${payload.dataId}`)),
), {dispatch: false});
In this example, the actual side effect is handled inside tap, and the configuration object {dispatch: false} passed to createEffect lets NgRx know that no action should be dispatched once the effect finishes its job.
Responding to Actions within a component
There may be cases where a component needs to react to an action that was dispatched from elsewhere in the application, particularly when that action relates to component-specific logic that doesn't belong in an effect class. One possible workaround is to inject the Actions stream into the component and subscribe to it directly:
@Component({
// component metadata
})
export class MyComponent implements OnInit {
constructor(
private readonly actions$: Actions,
) {}
ngOnInit() {
this.actions$.pipe(
ofType(someAction),
takeUntil(this.destroy$), // remember to unsubscribe
).subscribe(({payload}) => {
// perform some logic
});
}
}
While this approach does work, a more aligned strategy is to consider how the action influences the global state of the application. Namely, the reducer should update the state accordingly, and the component can subscribe to the relevant part of the Store via a selector to react to that change:
@Component({
// component metadata
})
export class MyComponent implements OnInit {
constructor(
private readonly store: Store,
) {}
ngOnInit() {
this.store.select(something) // state that reflects the need to perform an imperative action
.pipe(
takeUntil(this.destroy$), // again, unsubscribe
).subscribe(({payload}) => {
// perform some logic
});
}
}
Wrapping Up
NgRx Effects are a remarkably flexible feature of the NgRx ecosystem, enabling developers to stretch the capabilities of their state architecture and to oversee virtually every part of the application lifecycle through the lens of the Store. While the majority of effects rely on a basic, repetitive structure, there are times when more nuanced control becomes necessary — and those are the scenarios we explored here.
