Installing and integrating @ngrx/effects

Without redefining, let's look at the description of @ngrx/effects straight from the repo (https://github.com/ngrx/effects):

In @ngrx/effects, effects are the sources of actions. You use the @Effect() decorator to hint which observables on a service are action sources, and @ngrx/effects automatically merges your action streams, letting you subscribe them to store.

To help you compose new action sources, @ngrx/effects exports an action observable service that emits every action dispatched in your application.

In other words, we can chain our actions together with effects to provide powerful data flow composition throughout our app. They allow us to insert behavior that should take place between when an action is dispatched and before the state is ultimately changed. The most common use case is to handle HTTP requests and/or other asynchronous operations; however, they have many useful applications.

To use, let's first install @ngrx/effects:

npm i @ngrx/effects --save

Now let's take a look at what our user actions look like in an effect chain.

Real quickly, though, to remain consistent with our naming structure, let's rename auth.service.ts to user.service.ts. It helps when we have a naming standard that is consistent across the board.

Now, create app/modules/core/effects/user.effect.ts:

// angular
import { Injectable } from '@angular/core';

// libs
import { Store, Action } from '@ngrx/store';
import { Effect, Actions } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';

// module
import { LogService } from '../../core/services/log.service';
import { DatabaseService } from '../services/database.service';
import { UserService } from '../services/user.service';
import { UserActions } from '../actions/user.action';

@Injectable()
export class UserEffects {

@Effect() init$: Observable<Action> = this.actions$
.ofType(UserActions.ActionTypes.INIT)
.startWith(new UserActions.InitAction())
.map(action => {
const current = this.databaseService
.getItem(DatabaseService.KEYS.currentUser);
const recentUsername = this.databaseService
.getItem(DatabaseService.KEYS.recentUsername);
this.log.debug(`Current user: `, current || 'Unauthenticated');
return new UserActions.UpdatedAction({ current, recentUsername });
});

@Effect() login$: Observable<Action> = this.actions$
.ofType (UserActions.ActionTypes.LOGIN)
.withLatestFrom(this.store)
.switchMap(([action, state]) => {
const current = state.user.current;
if (current) {
// user already logged in, just fire updated
return Observable.of(
new UserActions.UpdatedAction({ current })
);
} else {
this._loginPromptMsg = action.payload.msg;
const usernameAttempt =
action.payload.usernameAttempt
|| state.user.recentUsername;

return Observable.fromPromise(
this.userService.promptLogin(this._loginPromptMsg,
usernameAttempt)
)
.map(user => (new UserActions.LoginSuccessAction(user)))
.catch (usernameAttempt => Observable.of(
new UserActions.LoginCanceledAction(usernameAttempt)
));
}
});

@Effect() loginSuccess$: Observable<Action> = this.actions$
.ofType(UserActions.ActionTypes.LOGIN_SUCCESS)
.map((action) => {
const user = action.payload;
const recentUsername = user.username;
this.databaseService
.setItem (DatabaseService.KEYS.currentUser, user);
this.databaseService
.setItem (DatabaseService.KEYS.recentUsername, recentUsername);
this._loginPromptMsg = null; // clear, no longer needed
return (new UserActions.UpdatedAction({
current: user,
recentUsername,
loginCanceled: false
}));
});

@Effect() loginCancel$ = this.actions$
.ofType(UserActions.ActionTypes.LOGIN_CANCELED)
.map(action => {
const usernameAttempt = action.payload;
if (usernameAttempt) {
// reinitiate sequence, login failed, retry
return new UserActions.LoginAction({
msg: this._loginPromptMsg,
usernameAttempt
});
} else {
return new UserActions.UpdatedAction({
loginCanceled: true
});
}
});

@Effect() logout$: Observable<Action> = this.actions$
.ofType(UserActions.ActionTypes.LOGOUT)
.map(action => {
this.databaseService
.removeItem(DatabaseService.KEYS.currentUser);
return new UserActions.UpdatedAction({
current: null
});
});

private _loginPromptMsg: string;

constructor(
private store: Store<any>,
private actions$: Actions,
private log: LogService,
private databaseService: DatabaseService,
private userService: UserService
) { }
}

We have clarified the intent of our data flow concerning our UserService and delegated the responsibility to this effect chain. This allows us to compose our data flow in a clear and consistent manner with a great deal of flexibility and power. For instance, our InitAction chain now allows us to automatically initialize the user via the following:

.startWith(new UserActions.InitAction())

Earlier, we were calling a private method--this._init()--inside the service constructor; however, we no longer need explicit calls like that as effects are run and queued up once the module is bootstrapped. The .startWith operator will cause the observable to fire off one single time (at the point of module creation), allowing the init sequence to be executed at a particularly opportune time, when our app is initializing. Our initialization sequence is the same as we were previously handling in the service; however, this time we're taking into consideration our new recentUsername persisted value (if one exists). We then end the init sequence with a UserActions.UpdatedAction:

new UserActions.UpdatedAction({ current, recentUsername })

Note that there's no effect chain wired to UserActions.ActionTypes.UPDATED. This is because there are no side effects that should occur by the time that Action occurs. Since there are no more side effects, the observable sequence ends up in the reducer that has a switch statement to handle it:

export function userReducer(
state: IUserState = userInitialState,
action: UserActions.Actions
): IUserState {
switch (action.type) {
case UserActions.ActionTypes.UPDATED:
return Object.assign({}, state, action.payload);
default:
return state;
}
}

This takes the payload (which is typed as the shape of the user state, IUserState) and overwrites the values in the existing state to return a brand new user state. Importantly, Object.assign allows any existing values in the source object to not be overridden unless explicitly defined by the incoming payload. This allows only new incoming payload values to be reflected on our state, while still maintaining the existing values.

The rest of our UserEffect chain is fairly self-explanatory. Primarily, it's handling much of what the service was previously handling, with the exception of prompting the login dialog, which the effect chain is utilizing the service method to do. However, it's worth mentioning that we can go so far as to completely remove this service as the contents of the promptLogin method can easily be carried out directly in our effect now.

When deciding if you should handle more logic in your effect or a designated service, it really comes down to personal preference and/or scalability. If you have rather lengthy service logic and more than a couple of methods to handle logic while working with effects, creating a designated service will help greatly. You can scale more functionality into the service without diluting the clarity of your effects chain.

Lastly, unit testing will be easier with a designated service with more logic. In this case, our logic is fairly simple; however, we'll leave the UserService for example purposes as well as best practice.

Speaking of, let's take a look at how simplified our UserService looks now
in app/modules/core/services/user.service.ts:

// angular
import { Injectable } from '@angular/core';

// app
import { DialogService } from './dialog.service';

@Injectable()
export class UserService {

constructor(
private dialogService: DialogService
) { }

public promptLogin(msg: string, username: string = ''): Promise<any> {
return new Promise((resolve, reject) => {
this.dialogService.login(msg, username, '').then((input) => {
if (input.result) { // result will be false when canceled
if (input.userName && input.userName.indexOf('@') > -1) {
if (input.password) {
resolve({
username: input.userName,
password: input.password
});
} else {
this.dialogService.alert('You must provide a password.')
.then(reject.bind(this, input.userName));
}
} else {
// reject, passing userName back to try again
this.dialogService.alert('You must provide a valid email
address.')
.then(reject.bind(this, input.userName));
}
} else {
// user chose cancel
reject(false);
}
});
});
}
}

It's much cleaner now. Okay, so how do we let our app know about all this new goodness?

First, let's follow one of our standards by adding an index to our entire core module; add app/modules/core/index.ts:

export * from './actions';
export * from './effects';
export * from './reducers';
export * from './services';
export * from './states';
export * from './core.module';

We simply export all the goodies our core module now provides, including the module itself.

Then, open app/modules/core/core.module.ts to finish our wiring:

// nativescript
import { NativeScriptModule } from 'nativescript-angular/nativescript.module';
import { NativeScriptFormsModule } from 'nativescript-angular/forms';
import { NativeScriptHttpModule } from 'nativescript- angular/http';

// angular
import { NgModule, Optional, SkipSelf } from '@angular/core';

// libs
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';

// app
import { UserEffects } from './effects';
import { userReducer } from './reducers';
import { PROVIDERS } from './services';
import { PROVIDERS as MIXER_PROVIDERS } from '../mixer/services';
import { PROVIDERS as PLAYER_PROVIDERS } from '../player/services';

const MODULES: any[] = [
NativeScriptModule,
NativeScriptFormsModule,
NativeScriptHttpModule
];

@NgModule({
imports: [
...MODULES,
// define core app state
StoreModule.forRoot({
user: userReducer
}),
// register core effects
EffectsModule.forRoot([
UserEffects
]),

],
providers: [
...PROVIDERS,
...MIXER_PROVIDERS,
...PLAYER_PROVIDERS
],
exports: [
...MODULES
]
})
export class CoreModule {
constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
if (parentModule) {
throw new Error(
'CoreModule is already loaded. Import it in the AppModule only');
}
}
}

Here we ensure that we define our user state key to use the userReducer and register it with StoreModule. We then call EffectsModule.forRoot(), with a collection of singleton effect providers to register like our UserEffects.

Now, let's take a look at how this improves the rest of the code base since we were undoubtedly injecting the UserService (previously named AuthService) in a couple of places.

We were previously injecting AuthService in AppComponent to ensure that Angular's dependency injection constructed it early on when the app was bootstrapped, creating the necessary singleton our app needed. However, with UserEffects automatically running now on bootstrap, which in turn injects (now renamed) UserService, we no longer need this rather silly necessity anymore, so, we can update AppComponent as follows:

@Component({
moduleId: module.id,
selector: 'my-app',
templateUrl: 'app.component.html',
})
export class AppComponent {

constructor() { // we removed AuthService (UserService) here

In one swoop, our code base is now getting smarter and slimmer. Let's keep going to see other benefits of our ngrx integration.

Open app/auth-guard.service.ts, and we can now make the following simplifications:

import { Injectable } from '@angular/core';
import { Route, CanActivate, CanLoad } from '@angular/router';

// libs
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';

// app
import { IUserState, UserActions } from '../modules/core';

@Injectable()
export class AuthGuard implements CanActivate, CanLoad {

private _sub: Subscription;

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

canActivate(): Promise<boolean> {
return new Promise ((resolve, reject) => {
this.store.dispatch(
new UserActions.LoginAction({ msg: 'Authenticate to record.' })
);
this._sub = this.store.select(s => s.user).subscribe((state:
IUserState) => {

if (state.current) {
this._reset();
resolve (true);
} else if (state.loginCanceled) {
this._reset ();
resolve(false);
}
});
});
}

canLoad(route: Route): Promise<boolean> {
// reuse same logic to activate
return this.canActivate();
}

private _reset() {
if (this._sub) this._sub.unsubscribe();
}
}

When activating the /record route, we dispatch the LoginAction every time since we require an authenticated user to use the recording features. Our login effects chain properly handles if the user is already authenticated, so all we need to do is set up our state subscription to react accordingly.

Ngrx is flexible, and how you set up your actions and effects chains is purely up to you.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.24.191