How to Handle Side Effects in Angular Using NgRx Effects

Dany ParedesDany Paredes
Jun 10, 2024·
7 min read

Side-effects! They are one of the most common tasks in our applications. In Angular, but build application if we don't take care the component ends with a lot of responsability, like get, process and render the data. But in Angular most of time when we need to get data from an API, instead of put the logic to handle everything related to HTTP requests, we create services to put the logic there, but our components still need to use these services to subscribe to them.

When we use Ngrx, the main idea is for components to trigger actions. These actions then cause the reducer to make the necessary changes in the state and get the updated data using the selectors in the component.

But how I can handle side-effect changes? For example start a http request, get the data and trigger the action with the result? who is responsible to get the data, process and update the state?

let's show a scenario, I need to show a list of players from my state, and the players come from an API. We have two actions to start this process: Players Load and Player Load Success.

export const HomePageActions = createActionGroup({
  source: 'Home Page',
  events: {
    'Accept Terms': emptyProps(),
    'Reject Terms': emptyProps(),
    'Players Load': emptyProps(),
    'Player Loaded Success': props<{ players: Array<any> }>(),
  },
});
​

We have to have a separation, so we create the players.service.ts with the responsibility to get the data.

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay, map } from 'rxjs';
import { environment } from '../../environments/environment';
import { Player } from '../entities/player';
​
@Injectable({ providedIn: 'root' })
export class PlayersService {
  private _http = inject(HttpClient);
​
  public getPlayers() {
    return this._http
      .get<{ data: Array<Player> }>(`${environment.apiUrl}/players`, {
        headers: {
          Authorization: `${environment.token}`,
        },
      })
      .pipe(
        map((response) => response.data),
        delay(5000),
      );
  }
}
​

But, I can't change the state in the reducer because it is a function, I can't do async task or dispatch actions from there, the only place available is to use the same component to dispatch the actions when get the data.

Open the home.component.ts, we inject the PlayersService, in the onOnInit lifecycle dispatch the HomePageActions.playersLoad() action to set the loading to true, and subscribe to the this._playersService.getPlayers() after get the data then dispatch the playerLoadedSuccess action with the response.

The code looks like:

export class HomeComponent implements OnInit {
  private _store = inject(Store);
  private _playersService = inject(PlayersService);
  public $loading = this._store.selectSignal(selectLoading);
  public $players = this._store.selectSignal(selectPlayers);
  /** 
    others properties removed to d to keep simplicity.
 **/public ngOnInit(): void {
    this._store.dispatch(HomePageActions.playersLoad());
    this._playersService.getPlayers().subscribe(players => {
      this._store.dispatch(HomePageActions.playerLoadedSuccess({
        players
      }))
    })
  }

The previous code works, but why does the home.component have to subscribe to the service and also dispatch the action when the data arrives? Why does the home.component need to know who is responsible for loading the data? The home component only needs to trigger actions and react to state changes.

This is where NgRx Effects are useful. They take actions, perform the necessary tasks, and dispatch other actions.

The Effects

What is an effect? It is a class like a service with the @Injectable decorator and the Actions injected. The Actions service help to listen each action dispatched after the reducer.

@Injectable()
export class HomeEffects {
      private _actions = inject(Actions);
}

We declare a field using the createEffect function, any action returned from the effect stream is then dispatch back to the Store and the Actions are filtered using a ofType operator to takes one or more actions. The of action is then flatten and mapped into a new observable using any high-orders operator like concatMap, exhaustMap , switchMap or mergeMap.

  loadPlayers = createEffect(() =>
    this._actions.pipe(
      ofType(HomePageActions.playersLoad)
));

Since the version 15.2 we also have functional effects, instead to use a class use the same createEffect function to create the effects.

export const loadPlayersEffect = createEffect(
  (actions$ = inject(Actions)) => {
});

But how does it work? Well, the component triggers the load product action, then the effect listens for this action. Next, we inject the service to get the data and trigger an action with the data. The reducer then listens for this action and makes the change.

Does it seem like too many steps? Let me show you how to refactor our code to use Effects!

Moving To Effects

It's time to start using effects in our project. We continue with the initial project of NgRx, clone it, and switch to the action-creators branch.

git clone https://github.com/danywalls/start-with-ngrx.git
git switch feature/using-selectors

Next, install effects @ngrx/effects package from the terminal.

 npm i @ngrx/effects

Next, open the project with your favorite editor and create a new file src/app/pages/about/state/home.effects.ts. Declare a loadPlayersEffect using the createEffect function, inject Actions and PlayersService, and then pipe the actions.

import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { PlayersService } from '../../../services/players.service';

export const loadPlayersEffect = createEffect(
  (actions$ = inject(Actions), playersService = inject(PlayersService)) => {
    return actions$.pipe()
  )
);

Use ofType to pipe the actions and filter by the HomePageActions.playersLoad action type.

  loadPlayers = createEffect(() =>
    this._actions.pipe(
      ofType(HomePageActions.playersLoad)
      )
  );

Use the concatMap operator to get the stream from the action, use the playerService and call the getPlayers() method and use map to dispatch HomePageActions.playerLoadedSuccess({ players }).

concatMap(() =>
        this._playersService
          .getPlayers()
          .pipe(
            map((players) => HomePageActions.playerLoadedSuccess({ players })),
          ),
      ),

After the map, handle errors using the catchError operator. Use the of function to transform the error into a HomePageActions.playerLoadFailure action and dispatch the error message.

catchError((error: { message: string }) =>
            of(HomePageActions.playerLoadFailure({ message: error.message })),
          ),

The final code looks like:

import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { PlayersService } from '../../../services/players.service';
import { HomePageActions } from './home.actions';
import { catchError, concatMap, map, of } from 'rxjs';

export const loadPlayersEffect = createEffect(
  (actions$ = inject(Actions), playersService = inject(PlayersService)) => {
    return actions$.pipe(
      ofType(HomePageActions.playersLoad),
      concatMap(() =>
        playersService.getPlayers().pipe(
          map((players) => HomePageActions.playerLoadedSuccess({ players })),
          catchError((error: { message: string }) =>
            of(HomePageActions.playerLoadFailure({ message: error.message })),
          ),
        ),
      ),
    );
  },
  { functional: true },
);

We have the effect ready, its time to register it in the app.config, so import the home.effect and use the provideEffects function pass the homeEffect

The app.config looks like:

import * as homeEffects from './pages/home/state/home.effects';

export const appConfig = {
  providers: [
    provideRouter(routes),
    provideStore({
      home: homeReducer,
    }),
    provideStoreDevtools({
      name: 'nba-app',
      maxAge: 30,
      trace: true,
      connectInZone: true,
    }),
    provideEffects(homeEffects), //provide the effects
    provideAnimationsAsync(),
    provideHttpClient(withInterceptors([authorizationInterceptor])),
  ],
};

We have registered the effect, so it's time to refactor the code in the HomeComponent. Remove the injection of the players service, as we no longer need to subscribe to the service.

The home component looks like:

export class HomeComponent implements OnInit {
  private _store = inject(Store);
  public $loading = this._store.selectSignal(selectLoading);
  public $players = this._store.selectSignal(selectPlayers);
  public $acceptTerms = this._store.selectSignal(selectAcceptTerms);
  public $allTasksDone = this._store.selectSignal(selectAllTaskDone);
​
  public ngOnInit(): void {
    this._store.dispatch(HomePageActions.playersLoad());
  }
​
  onChange() {
    this._store.dispatch(HomePageActions.acceptTerms());
  }
​
  onRejectTerms() {
    this._store.dispatch(HomePageActions.rejectTerms());
  }
}

Done! Our app is now using effects, and our components are clean and organized!

Recap

We learned how to handle side-effects like HTTP requests and clean up components that have too many responsibilities. By using actions, reducers, and effects to manage state and side-effects. We can refactor our component to use NgRx Effects for fetching data from an API. By moving the data-fetching logic to effects, components only need to dispatch actions and react to state changes, resulting in cleaner and more maintainable code.

16
Subscribe to my newsletter

Read articles from Dany Paredes directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dany Paredes
Dany Paredes

I'm passionate about front-end development, specializing in building UI libraries and working with technologies like Angular, NgRx, Accessibility, and micro-frontends. In my free time, I enjoy writing content for the Google Dev Library, This Is Angular Community, Kendo UI, and sharing insights here.