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
Add
login$
Effect toLoginEffects
.Dispatch
LoginActions.loginSuccess
Action when API request is successful.Dispatch
LoginActions.loginFailure
Action when API request fails.Add
logout$
Effect toLoginEffects
.Dispatch
LoginActions.logoutSuccess
Action when API request is successful.Dispatch
LoginActions.logoutFailure
Action when API request fails.
Problem 1: Create login$
Effect to Handle API Requests
LoginEffects
should create a login API request usingngx-learn-ngrx
’sLoginService.login()
method whenever theLoginActions.login
Action is dispatched using an Effect calledlogin$
.If the API request is successful, a
LoginActions.loginSuccess
Action should be dispatched using the API response.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:
createEffect()
helper function to create Effects.ofType()
helper function to filter Actions bytype
.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:
password
must be at least 6 characters.username
must be at least 3 characters.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
LoginEffects
should create a logout API request usingngx-learn-ngrx
’sLoginService.logout()
method whenever theLoginActions.logout
Action is dispatched using an Effect calledlogout$
.If the API request is successful, a
LoginActions.logoutSuccess
Action should be dispatched using the API response.If the API request is unsuccessful, a
LoginActions.logoutFailure
Action should be dispatched using the samegetErrorMessage()
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