Creating Redirect Effects page

Learn how to create NgRx Effects that redirect the user.

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

Overview

  1. Add loginSuccess$ Effect to LoginEffects to navigate to dashboard page.

  2. Add logoutSuccess$ Effect to LoginEffects to navigate to login page.

Problem 1: Create loginSuccess$ Effect to Handle Navigating to Dashboard Page

LoginEffects should use Router to navigate to the dashboard page at path /dashboard using an Effect called loginSuccess$.

P1: What you need to know

Although it’s common for Effects to dispatch another Action after handling a side-effect, there is a way to update the configuration for an Effect to never dispatch an Action instead. This is useful when a side-effect resolves and there’s no need to trigger another side-effect or update state.

To understand this better, let’s take a deeper dive into the createEffect() helper function:

createEffect() takes two arguments:

  1. source - A function which returns an Observable.

  2. config (optional) - A Partial<EffectConfig> to configure the Effect. By default, dispatch option is true and useEffectsErrorHandler is true.

And if we look a little deeper, here is the type definition of the EffectConfig interface:

/**
 * Configures an Effect created by `createEffect()`.
 */
export interface EffectConfig {
  /**
   * Determines if the Action emitted by the Effect is dispatched to the store.
   * If false, Effect does not need to return type `Observable<Action>`.
   */
  dispatch?: boolean;
  /**
   * Determines if the Effect will be resubscribed to if an error occurs in the main Actions stream.
   */
  useEffectsErrorHandler?: boolean;
}

By default, the dispatch option is set to true, but if we set it to false, the Effect doesn’t have to end with an Action being dispatched:

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

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

@Injectable()
export class ContactEffects {
  submitSuccess$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(ContactActions.submitSuccess),
      exhaustMap(({ confirmationNumber }) =>
        // call `showModal()`, but doesn’t `map` to some Action
        this.showModal(confirmationNumber)
      )
    );
  }, { dispatch: false });
 
  constructor(private actions$: Actions) {}

  private showModal(confirmationNumber: number): Observable<void> {/* ... */}
}

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';
import { Router } from '@angular/router';

@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),
              })
            )
          )
        )
      )
    );
  });

  loginSuccess$ = createEffect(
    () => {
      return this.actions$.pipe(
        ofType(LoginActions.loginSuccess),
        exhaustMap(() => this.router.navigate(['dashboard']))
      );
    },
    { dispatch: false }
  );

  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 router: Router
  ) {}

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

Problem 2: Create logoutSuccess$ Effect to Handle Navigating to Login Page

LoginEffects should use Router to navigate to the dashboard page at path / using an Effect called loginSuccess$.

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';
import { Router } from '@angular/router';

@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),
              })
            )
          )
        )
      )
    );
  });

  loginSuccess$ = createEffect(
    () => {
      return this.actions$.pipe(
        ofType(LoginActions.loginSuccess),
        exhaustMap(() => this.router.navigate(['dashboard']))
      );
    },
    { dispatch: false }
  );

  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),
              })
            )
          )
        )
      )
    );
  });

  logoutSuccess$ = createEffect(
    () => {
      return this.actions$.pipe(
        ofType(LoginActions.logoutSuccess),
        exhaustMap(() => this.router.navigate(['']))
      );
    },
    { dispatch: false }
  );

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

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

Verify Implementation

At this point, you should be able to login on the login page:

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 LoginService.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.

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-redirect-effects