Create an API Effect page

Create an NgRx Effect to connect the Login Store to an authentication API.

Quick Start: You can checkout this branch to get your codebase ready to work on this section.

Overview

  1. Add login$ Effect to LoginEffects.

  2. Dispatch LoginActions.loginSuccess Action when API request is successful.

  3. Dispatch LoginActions.loginFailure Action when API request fails.

  4. Add logout$ Effect to LoginEffects.

  5. Dispatch LoginActions.logoutSuccess Action when API request is successful.

  6. Dispatch LoginActions.logoutFailure Action when API request fails.

Problem 1: Create login$ Effect to Handle API Requests

  1. LoginEffects should create a login API request using ngx-learn-ngrx’s LoginService.login() method whenever the LoginActions.login Action is dispatched using an Effect called login$.

  2. If the API request is successful, a LoginActions.loginSuccess Action should be dispatched using the API response.

  3. If the API request is unsuccessful, a LoginActions.loginFailure Action should be dispatched using the following method to parse the error to a message:

src/app/store/login/login.effects.ts

// src/app/store/login/login.effects.ts

import { Injectable } from '@angular/core';
import { Actions } from '@ngrx/effects';

@Injectable()
export class LoginEffects {
  constructor(private actions$: Actions) {}

  private getErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }
    return String(error);
  }
}

P1: What you need to know

NgRx Effects are a side-effect model that utilizes RxJS to react to Actions being dispatched to manage side-effects such as network requests, web socket messages and time-based events. One thing that Effects are not responsible for is updating state; this is a responsiblity for Reducers (Create a Reducer).

Note that for a given Action, Effects will always happen after the state has been updated by the Reducer.

NgRx provides a couple of helpful functions and the Actions class to create Effects:

  1. createEffect() helper function to create Effects.

  2. ofType() helper function to filter Actions by type.

  3. Actions class that extends the RxJS Observable to listen to every dispatched Action.

// Note: This example code is not part of our application repo or solution

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as ContactActions from './contact.actions';

@Injectable()
export class ContactEffects {
  submit$ = createEffect(() => {// Create Effect
    return this.actions$.pipe(// Listen for all dispatched Actions
      ofType(ContactActions.submit),// Filter for submit Action
      // ...
    );
  });
 
  constructor(private actions$: Actions) {}

  private handleSubmit(emailAddress: string, fullName: string): Observable<number> {/* ... */}

  private getErrorMessage(error: unknown): string {/* ... */}
}

Flattening RxJS Operators

"Flattening" operators are commonly used when creating Effects since we will likely use Observables or Promises to make API requests or perform some asynchronous task to produce some kind of side-effect.

One way to subscribe to an "inner" Observable within an existing "outer" Observable stream is to use "flattening" operators such as mergeMap, switchMap, or exhaustMap. These "flattening" operators can also allow us to use Promises within our Observable stream as well.

Although we could use any of these "flattening" operators as a working solution, we will be using exhaustMap. Each of the "flattening" operators have a slightly different behavior when handling multiple "inner" Subscriptions. For exhaustMap, each new "inner" Subscription is ignored if there is an existing "inner" Subscription that hasn’t completed yet:

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>

<script type="typescript">
  const { map, exhaustMap, interval, take } = rxjs;

  const outerObservable = interval(1000).pipe(map((n) => `Outer: ${n}`));

  outerObservable.pipe(
    exhaustMap((outerValue) =>
      interval(1000).pipe(
        map((innerValue) => `${outerValue} Inner: ${innerValue}`),
        take(3)// Complete Inner Subscription after 3 values
      )
    )
  ).subscribe((x) => console.log(x));
</script>

As you can see by running above CodePen, only one active Subscription can exist at one time. A new Subscription can only be made once the active Subscription completes:

Expected Output:

Outer: 0 Inner: 0 <-- New "inner" Subscription
Outer: 0 Inner: 1
Outer: 0 Inner: 2
Outer: 4 Inner: 0 <-- New "inner" Subscription
Outer: 4 Inner: 1
Outer: 4 Inner: 2
Outer: 8 Inner: 0 <-- New "inner" Subscription
Outer: 8 Inner: 1
Outer: 8 Inner: 2

Taking advantage of these tools provided by NgRx, we can create API requests and dispatch a new Action using the API response:

// Note: This example code is not part of our application repo or solution

import { Injectable } from '@angular/core';
import { Observable, catchError, map, exhaustMap } from 'rxjs/operators';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as ContactActions from './contact.actions';

@Injectable()
export class ContactEffects {
  submit$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(ContactActions.submit),
      exhaustMap(({ emailAddress, fullName }) =>
        this.handleSubmit(emailAddress, fullName).pipe(
          map((confirmationNumber) =>
            // Return an Action on success
            ContactActions.submitSuccess({ confirmationNumber })
          )
        )
      )
    );
  });
 
  constructor(private actions$: Actions) {}

  private handleSubmit(emailAddress: string, fullName: string): Observable<number> {/* ... */}

  private getErrorMessage(error: unknown): string {/* ... */}
}

We can also perform error handling and dispatch a new Action when an error occurs:

// Note: This example code is not part of our application repo or solution

import { Injectable } from '@angular/core';
import { Observable, catchError, map, exhaustMap } from 'rxjs/operators';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import * as ContactActions from './contact.actions';

@Injectable()
export class ContactEffects {
  submit$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(ContactActions.submit),
      exhaustMap(({ emailAddress, fullName }) =>
        this.handleSubmit(emailAddress, fullName).pipe(
          map((confirmationNumber) =>
            ContactActions.submitSuccess({ confirmationNumber })
          ),
          catchError((error: unknown) =>
            of(
              // Return an Action on failure
              ContactActions.submitFailure({
                errorMsg: this.getErrorMessage(error),
              })
            )
          )
        )
      )
    );
  });
 
  constructor(private actions$: Actions) {}

  private handleSubmit(emailAddress: string, fullName: string): Observable<number> {/* ... */}

  private getErrorMessage(error: unknown): string {/* ... */}
}

And last, you will need to use ngx-learn-ngrx’s LoginService to perform authentication for course:

// src/app/store/login/login.effects.ts

import { Injectable } from '@angular/core';
import { Actions } from '@ngrx/effects';
import { LoginService } from 'ngx-learn-ngrx';

@Injectable()
export class LoginEffects {
  constructor(private actions$: Actions, private loginService: LoginService) {}

  private getErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }
    return String(error);
  }
}

LoginService has 2 methods login() and logout() that will be needed to management authentication for this course.

Note that authenication DOES NOT persist after a page refresh. This means that after you make code changes while serving the application, you will be signed out and will need to login again. Remember that the login page is located at /.

The login() method will throw an error if any of these cases are not met:

  1. password must be at least 6 characters.

  2. username must be at least 3 characters.

  3. username must be alphanumeric including hyphens or underscores.

When one of these requirements aren’t met, an error is thrown and an error is logged in the console in red text.

P1: Solution

src/app/store/login/login.effects.ts

// src/app/store/login/login.effects.ts

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, exhaustMap } from 'rxjs/operators';
import { of } from 'rxjs';
import * as LoginActions from './login.actions';
import { LoginService } from 'ngx-learn-ngrx';

@Injectable()
export class LoginEffects {
  login$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(LoginActions.login),
      exhaustMap(({ username, password }) =>
        this.loginService.login({ username, password }).pipe(
          map(({ userId, token }) =>
            LoginActions.loginSuccess({ userId, username, token })
          ),
          catchError((error: unknown) =>
            of(
              LoginActions.loginFailure({
                errorMsg: this.getErrorMessage(error),
              })
            )
          )
        )
      )
    );
  });

  constructor(private actions$: Actions, private loginService: LoginService) {}

  private getErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }
    return String(error);
  }
}

Problem 2: Create logout$ Effect to Handle API Requests

  1. LoginEffects should create a logout API request using ngx-learn-ngrx’s LoginService.logout() method whenever the LoginActions.logout Action is dispatched using an Effect called logout$.

  2. If the API request is successful, a LoginActions.logoutSuccess Action should be dispatched using the API response.

  3. If the API request is unsuccessful, a LoginActions.logoutFailure Action should be dispatched using the same getErrorMessage() method to parse the error to a message.

P2: Solution

src/app/store/login/login.effects.ts

// src/app/store/login/login.effects.ts

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, exhaustMap } from 'rxjs/operators';
import { of } from 'rxjs';
import * as LoginActions from './login.actions';
import { LoginService } from 'ngx-learn-ngrx';

@Injectable()
export class LoginEffects {
  login$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(LoginActions.login),
      exhaustMap(({ username, password }) =>
        this.loginService.login({ username, password }).pipe(
          map(({ userId, token }) =>
            LoginActions.loginSuccess({ userId, username, token })
          ),
          catchError((error: unknown) =>
            of(
              LoginActions.loginFailure({
                errorMsg: this.getErrorMessage(error),
              })
            )
          )
        )
      )
    );
  });

  logout$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(LoginActions.logout),
      exhaustMap(() =>
        this.loginService.logout().pipe(
          map(() => LoginActions.logoutSuccess()),
          catchError((error: unknown) =>
            of(
              LoginActions.logoutFailure({
                errorMsg: this.getErrorMessage(error),
              })
            )
          )
        )
      )
    );
  });

  constructor(private actions$: Actions, private loginService: LoginService) {}

  private getErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }
    return String(error);
  }
}

Wrap-up: By the end of this section, your code should match this branch. You can also compare the code changes for our solution to this section on GitHub or you can use the following command in your terminal:

git diff origin/create-api-effects