DEV Community

Cover image for Manipulating NgRx Effects
Armen Vardanyan for This is Angular

Posted on

Manipulating NgRx Effects

Original cover photo by Josh Redd on Unsplash.

NgRx Effects are a popular part of NgRx, a state management library built for Angular. Effects are usually used to perform side effects like fetching data using HTTP, WebSockets, writing to browser storage, and more.

Most effects are straightforward: they receive a triggering action, perform a side effect, and return an Observable stream of another action which indicates the result is ready. NgRx effects will then automatically dispatch that action to trigger the reducers and perform a state change.

But sometimes, it is needed to perform some state-related logic in Effects, and RxJS operators are going to help us a lot. Let’s explore some scenarios.

Disallowing an Effect depending on data from the Store

Sometimes, some Effects should not run if the user, for example, has invalid permissions, or depending on some other form of state from the Store. Of course, the Store itself is an Observable, so we can use that to perform runtime logic and checkings in an Effect. Here is an example:

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))),
    ))
  ));
Enter fullscreen mode Exit fullscreen mode

In this example, we need some specific permission to exist in order to allow any HTTP call. We do this in 3 steps:

  1. Use “withLatestFrom” to get the latest data n permission through a selector from the Store
  2. Use the “filter” operator to disallow cases when the permission is not present
  3. Use the “map” operator to return only the action, as we no longer need the value from permissions

Let’s write a custom operator so that we can reuse this logic later in other cases, and also to reduce repetitive code. Our operator must

  1. Accept an Observable of a boolean value to determine whether to stop the effect from running or not
  2. Use that value to filter out the emissions
  3. Map back to the action

Here is an example of how this can be done:

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),
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

We can the use this in our Effect 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))),
    ))
  ));
Enter fullscreen mode Exit fullscreen mode

Making sure some data exists before an effect

Sometimes there are situations where some components might trigger an effect, but we need some other data before we can proceed, but that data may or may not be present at the moment the action is dispatched. What we want is for some effect to wait until that data arrives, and only then proceed with the side effect.

The natural instinct might tell us to use the “skipUntil” operator, but it has one obvious problem: “skipUntil” disallows the emissions until another Observable fires. But after the other Observable fires, the source must emit once again to resume working normally, so we will have to wait for another action dispatch in our case, to proceed.

So ideally we want “wait for the other Observable to emit and proceed with this one emission”, rather than “skip if the other one has not emitted yet”. Here is how we can do it:

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 }))),
      )),
    ))
  ));
Enter fullscreen mode Exit fullscreen mode

In this Effect, we receive an action, but rather than handle it right away, we switch to another Observable, which selects data from the store and checks that it is present. If it is not present, our pipeline will wait, but when the data finally arrives, it will proceed with our action from before and actually handle the side effect.

Handling a pure side effect

Sometimes we need to perform some actions that do not impact the store, but constitute a side effect. For example, we might want to redirect to a certain page when some effect is handled, without performi anything on the Store itself. So we might not want to map our effect to any particular action. We can achieve this by using the “tap” operator and a special option for the “createEffect” function:

redirectAfterDataSaved$ = createEffect(() => this.actions$.pipe(
    ofType(dataSavedSuccess),
    tap(({payload}) => this.router.navigateByUrl(`some-page/${payload.dataId}`)),
  ), {dispatch: false});
Enter fullscreen mode Exit fullscreen mode

In this example, we use tap to perform the side effect itself, and pass “{dispatch: false}” to the “createEffect” function to indicate it does not need to dispatch anything after the side effect completes.

Using Actions in components

Sometimes we might need to perform something in a component based on an action from another place. Because it touches component specific data, we cannot do that in an Effect class. But we can have a workaround: inject the “Actions” Observable into a component and subscribe to it:

@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
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Hint: although this works, a better approach to this will be to understand how this change reflects on the overall state of the application in the Store, use the reducer function to make the modification on the state and subscribe to the Store via a selector and perform the logic in the component:

@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
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

In Conclusion

NgRx Effects are a powerful tool that allows us to greatly expand the capabilities of our Store architecture and basically maintain all the life cycles of our app through the prism of NgRx Store. While most effects are pretty basic, there is sometimes a need for more advanced manipulations, which we covered in this article.

Latest comments (1)

Collapse
 
wojtrawi profile image
Wojciech Trawiński

When it comes to Using Actions in components, an alternative to injecting the actions observable into component, is to have a service for communication with a component and interact with it in an effect. I had a similar scenario with a component implementing an infinite scroll which had to manipulate the scroll position after actions like changeSortBy, changeGroupBy etc. I used a reactive service for communication with the component.