Working with NgRx Effects
Tags: angular, typescript, ngrx, webdev
Published: 2021-04-12 09:16:57

Almost every web application will, at some point, need to interact with some external resources. The most classic solution to that would be a service-based approach where components are calling and interacting with the external resources directly through services. In this case, most of the heavy lifting is delegated to the services and a component in this scenario still carries a responsibility to directly initiate those interactions. 

[NgRx Effects](https://ngrx.io/guide/effects) provides us with a way to isolate interactions, with the aforementioned services, from the components. Within Effects, we can manage various tasks ie. communication with the API, long-running tasks, and practically every other external interaction. In this scenario, the component doesn't need to know about these interactions at all. Its only requires some input data and then emits simple events (actions).

In this article, we will build on top of the application we started in [Introduction to NgRx](https://labs.thisdot.co/blog/introduction-to-ngrx). You can find the entry point for this article on my [GitHub repo](https://github.com/ktrz/introduction-to-ngrx/tree/effects_entryPoint). If you want to follow this article's code, please clone the repository and checkout the `effects_entryPoint` tag.

```
git clone git@github.com:ktrz/introduction-to-ngrx.git

git checkout effects_entryPoint
```

After cloning, just install all the dependencies. 
```
yarn install
```
and you can see the example app by running
```
yarn start -o
```

## Getting started

In order to add NgRx Effects to our application, all we need to do is use the `ng add` functionality offered by the [Angular CLI](https://cli.angular.io/). Run the following command:

```sh
ng add @ngrx/effects@latest
```

It will add and install the `@ngrx/effects` library to your `package.json` and scaffold your `AppModule` to import the NgRx `EffectsModule` into your application.

This is the code that the Angular CLI will generate for you:

```ts
/* Other imports */
import { EffectsModule } from '@ngrx/effects';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    /* Other imported modules */
    EffectsModule.forRoot([]),
  ],
  bootstrap: [AppComponent]
})
export class AppModule {
}
```

With the setup complete, we can start modifying the app to introduce and handle some API calls using Effects.

## Design interactions - Actions & Reducers

When you're designing new features, I highly encouarge you to first create the actions which we expect to see in the application. Let's look at the example API, which you can clone and checkout: `effects_ready` branch from [this repo](https://github.com/ktrz/ngrx-photos-api/tree/effects_ready). Then, use the `npm start` command to run it locally.

The API consists of the following endpoints:

`GET /api/photos` - returns an array of photos
`PUT /api/photos/:photoId/like` - returns the photo that was liked
`PUT /api/photos/:photoId/dislike` - returns photo that was disliked

We can start designing our app interactions by handling how the list of photos is loaded. First, we'll need a trigger action to start fetching the list of photos. Since the request can either return successfully, or with an error, let's model that as well within the actions:

```ts
// src/app/store/photo.actions.ts
import {createAction, props} from '@ngrx/store';
import {Photo} from '../photo/photo';

export const loadPhotos = createAction('[Photo List] Load Photos');
export const loadPhotosSuccess = createAction('[Photo List] Load Photos Success', props<{photos: Photo[]}>());
export const loadPhotosError = createAction('[Photo List] Load Photos Error');
```

We have modeled the actions that might occur in the application. Now it's time to handle them properly in the `photo.reducer.ts`.

```ts
// src/app/store/photo.reducer.ts

/* other imports */
import {loadPhotosSuccess} from './photo.actions';

const initialState: PhotoState = {};

export const photoReducer = createReducer(
  initialState,
  /* previous `on` handlers */
  on(loadPhotosSuccess, (state, {photos}) => photos.reduce((acc, photo) => ({
    ...acc,
    [photo.id]: photo
  }), {}))
);
```

Since we're getting an array of photos, and we're keeping them in the state as an id-indexed map, we just need to transform it into the appropriate shape. Since we assume that the API returns all of the photos, we can replace the whole previous state.

Great! We now have a correctly working reducer. However, we don't actually emit any action that will put the data in our Store anywhere in our application. To verify that it works correctly, we can dispatch `loadPhotosSuccess` action in our `AppComponent`:

```ts
// src/app/app.component.ts

export class AppComponent implements OnInit {
  photos$ = this.store.select(selectPhotos);

  constructor(private store: Store<AppState>) {
  }

  ngOnInit(): void {
    this.store.dispatch(loadPhotosSuccess({
      photos: [
        {
          id: '2d335401-d65e-4059-b8f0-a4816c82086f',
          title: 'Introduction to NgRx',
          url: 'https://ngrx.io/assets/images/ngrx-badge.png',
          likes: 0,
          dislikes: 0,
        },
        {
          id: '65a7eb36-f887-4a93-8fe7-38d20c77906f',
          title: 'Angular',
          url: 'https://angular.io/assets/images/logos/angular/angular.png',
          likes: 0,
          dislikes: 0,
        }
      ]
    }));
  }
}
```

The data is loaded correctly and all the other functionality is still working as expected. Let's revert this dispatch so we can finally create our Effects, which will allow our available photos to asynchronously load.

## Create Effects

In NgRx, Effects are encapsulated in a regular Angular `Injectable` class. To let NgRx know to use our class as Effects, we need to add an `EffectsModule.forRoot([])` array inside of our `AppModule` imports:

```ts
// src/app/store/photo.effects.ts

import {Actions} from '@ngrx/effects';
import {PhotoService} from '../api/photo.service';
import {Injectable} from '@angular/core';

@Injectable()
export class PhotoEffects {
  constructor(
    private actions$: Actions, // this is an RxJS stream of all actions
    private photoService: PhotoService // we will need this service for API calls
  ) {}
}
```

```ts
// src/app/app.module.ts

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    / * other imports */
    EffectsModule.forRoot([PhotoEffects]),
    // this is necessary for `PhotoService` to have access to the HttpClient
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
```

Inside of the `PhotoEffects`, we will create properties that will react to specific actions being dispatched, perform some side effect (in this case an API call), and susequently dispatch another action based on the API call result. This flow is presented in the following diagram:

![Effects diagram](//images.ctfassets.net/zojzzdop0fzx/4VcryqxvMShD6qzVgKuJJi/ab8da8993cf514b92ecd50e6d66e02cd/Effects_diagram.png)

In our case, we will listen for the `loadPhotos` action being dispatched. Then, we will call the `PhotoService -> getPhotos()` method, which should either return the correct data, or return an error (ie. a network error). Upon receiving data, we can dispatch the `loadPhotosSuccess` action, and in order to handle possible errors, we might dispatch `loadPhotosError`:

```ts
// src/app/store/photo.effects.ts

import {Actions, createEffect, Effect, ofType} from '@ngrx/effects';
import {PhotoService} from '../api/photo.service';
import {Injectable} from '@angular/core';
import {loadPhotos, loadPhotosError, loadPhotosSuccess} from './photo.actions';
import {catchError, map, switchMap} from 'rxjs/operators';
import {of} from 'rxjs';

@Injectable()
export class PhotoEffects {
  loadPhotos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadPhotos),
      switchMap(() => this.photoService.getPhotos().pipe(
        map(photos => loadPhotosSuccess({photos})),
        catchError(() => of(loadPhotosError()))
      ))
    )
  );

  constructor(
    private actions$: Actions,
    private photoService: PhotoService
  ) {}
}
```

The app still doesn't do anything. That's because we need the `loadPhotos` action to be dispatched somewhere. We can do it on the `AppComponent` initialization inside of `ngOnInit` lifecycle hook.

```ts
// src/app/app.component.ts

export class AppComponent implements OnInit {
  /* Rest of the component */

  constructor(private store: Store<AppState>) {}

  ngOnInit(): void {
    this.store.dispatch(loadPhotos());
  }

  /* Rest of the component */
}
```

If we look at our application again, we can see that the correct data has loaded. In the network tab of the Dev Tools, we can see the correct API being called. Liking/disliking still works, at least until we refresh the page. We still don't perform any API calls when we like or dislike a photo. Let's implement that behavior similarly to how we implemented photo loading.

The easiest way to accomplish this is by treating the `likePhoto`and `dislikePhoto` actions as triggers for the API call, and upon a successful or failed response, emitting a new action. Let's name those `updatePhotoSuccess` and `updatePhotoError`:

```ts
// src/app/store/photo.actions.ts

import {createAction, props} from '@ngrx/store';
import {Photo} from '../photo/photo';

/* other actions */

export const updatePhotoSuccess = createAction('[Photo List] Update Photo Success', props<{photo: Photo}>());
export const updatePhotoError = createAction('[Photo List] Update Photo Error');
```

Now, in reducer, instead of having separate handling for like and dislike, we can replace it with a single handler for `updatePhotoSuccess`

```ts
// src/app/store/photo.reducer.ts

/* Rest of the file */

const initialState: PhotoState = {};

export const photoReducer = createReducer(
  initialState,
  on(updatePhotoSuccess, (state, {photo}) => ({
    ...state,
    [photo.id]: photo
  })),
  on(loadPhotosSuccess, (state, {photos}) => photos.reduce((acc, photo) => ({
    ...acc,
    [photo.id]: photo
  }), {}))
);
```

Now, with all actions and reducers in place, all that is left to do is add a new effect responsible for performing API call and emitting a new action for updating the state.

```ts
@Injectable()
export class PhotoEffects {
  / * loadPhotos$ effect */

  likePhoto$ = createEffect(() => this.actions$.pipe(
    ofType(likePhoto),
    mergeMap(({id}) => this.photoService.likePhoto(id).pipe(
      map(photo => updatePhotoSuccess({photo})),
      catchError(() => [updatePhotoError()])
    ))
  ));

  dislikePhoto$ = createEffect(() => this.actions$.pipe(
    ofType(dislikePhoto),
    mergeMap(({id}) => this.photoService.dislikePhoto(id).pipe(
      map(photo => updatePhotoSuccess({photo})),
      catchError(() => [updatePhotoError()])
    ))
  ));

  /* constructor */
}
```

## Conclusion

Now, all the functionality is still working, and our data is kept safely on the server. All of this was done without modifying the component's code (except for initial dispatch of `loadPhotos`). That means we can add some complex logic for how we handle data (ie. add data polling, optimistic update, caching etc.) without requiring the components to know about this. This enables us to keep the codebase cleaner and much easier to maintain.

You can find the code for this article's end result on my GitHub repos:
* [Angular app](https://github.com/ktrz/introduction-to-ngrx/tree/effects_ready)
* [Photos API app](https://github.com/ktrz/ngrx-photos-api/tree/effects_ready)
Checkout `effects_ready` tag to get the up-to-date and ready-to-run solution.

In case you have any questions you can always tweet or DM me [@ktrz](https://twitter.com/ktrz__). I'm always happy to help!

---

_This Dot Labs is a modern web consultancy focused on helping companies realize their digital transformation efforts. For expert architectural guidance, training, or consulting in React, Angular, Vue, Web Components, GraphQL, Node, Bazel, or Polymer, visit [thisdotlabs.com](https://www.thisdotlabs.com)._

_This Dot Media is focused on creating an inclusive and educational web for all. We keep you up to date with advancements in the modern web through events, podcasts, and free content. To learn, visit [thisdot.co](https://www.thisdot.co)._