10
votes

I'm making an Angular 2 app with @ngrx/store and @ngrx/effects.

I'm struggling with understanding where to put logic outside of actions/effects etc and where to call service functions.

For example, with authentication...

  1. When a user clicks login this dispatches an AUTH_REQUEST action with the login credentials as the payload.
  2. An effect looks for this action an calls the API.
  3. A successful result calls the AUTH_SUCCESS action with the token, username etc in the response object as a payload which goes to the reducer to update the AuthState.

eg: In AuthEffects

@Effect() authenticate$ = this.updates$
.whenAction(AuthActions.AUTHENTICATE_REQUEST)
.switchMap(update => this.api.post('/authenticate', update.action.payload)
  .map((res:any) => ({type: AuthActions.AUTHENTICATE_SUCCESS, payload: res.json()}))
  .catch((err:any) => Observable.of({ type: AuthActions.AUTHENTICATE_ERROR, payload: err }))
);

In AuthReducer

 case AuthActions.AUTHENTICATE_SUCCESS:
  return Object.assign({}, state, <AuthState>{
    processing: false,
    failed: false,
    isLoggedIn: true,
    token: action.payload.token,
    username: action.payload.username,
    accountId: action.payload.accountId,
  });

What I want to know is:

  1. Where to call the router to change pages after an AUTH_SUCCESS action is processed. Do I do this from within the effects Reactive chain or....??
  2. I have a AuthService that needs to store the credentials (token etc) in LocalStorage. Where should I call this to "store the token" ie authService.store(userCredentials).

Any help appreciated.

3
Yes I am using store router...markstewie

3 Answers

4
votes

Clearly, this isn't a definitive answer; it's just what I chose to do.

The CodeSequence/ngrx-‌​store-router implements actions for the v3 router. However, it doesn't implement a action creator - just the string types. I used a simple action creator so that I don't have to have to have action literals all over the place:

import * as ngrxStoreRouter from "ngrx-store-router";

@Injectable()
export class RouterActions {

    static NAVIGATE: string = ngrxStoreRouter.RouterActions.navigating;
    navigate(url: string): Action {

        return {
            payload: { url },
            type: RouterActions.NAVIGATE
        };
    }
}

And I used an effects class to dispatch the router actions:

@Injectable()
export class RouterEffects {

    ...

    @Effect()
    createUser(): Observable<Action> {

        return this.stateUpdates_
            .whenAction(AuthActions.CREATE_USER_SUCCESS)
            .map((update) => update.action.payload)
            .switchMap((payload) => {

                return Observable.of(this.routerActions_.navigate("/on-board"));
            });
    }

    ...
}

My reasoning was this doesn't involve the authentication effects knowing anything about routing, it makes it easy to write tests for router effects, and the router actions fit in nicely with the @ngrx/store-devtools.

Regarding your second question, I'd be inclined to wire it up to a ..._SUCCESS action in an effect.

I'd be interested in hearing about alternative approaches.

0
votes

There has been also a ngrx "official" library handling these type of functionalities, @ngrx/router-store. Right now @ngrx/router is being deprecated in favour of @angular/router 3.0.0 (now in RC1), as indicated by their migration guide. As a consequence, router-store is undergoing changes as well, to migrate to angular router, and there's a pending PR addressing that. When everything hopefully settles a bit, router-store provides an action creator for navigation purposes, very handy to return from effects:

// ...
import { Effect, toPayload, StateUpdates } from '@ngrx/effects';
import { go } from '@ngrx/router-store';

// ... in custom effects class

@Effect() loginSuccess$: Observable<Action> = this.updates$
  .whenAction(LoginActions.LOGIN_SUCCEEDED)
  .map<ActionPayloads.LoginSucceeded>(toPayload)
  .do(successPayload => {
    // grab auth information you need to store
    // and save them now, accessing your local storage service

    const { authInfo, rememberMe } = successPayload;

    this.authStorage.save(authInfo, rememberMe);
  })
  .map(_ => go(homeRoutePath));

Right now it seems this does not work, but it should be back as soon as router-store is updated. HTH

0
votes

I make it this way:

import { go } from '@ngrx/router-store';
...

@Injectable()
export class AuthEffects {

  constructor(
    private actions$: Actions,
    private authService: AuthService
  ) { }

  @Effect() loginSuccess$: Observable<Action> = this.actions$
    .ofType(auth.ActionTypes.LOGIN_SUCCESS)
    .map((action) => action.payload as AuthUser)
    .do((user: AuthUser) => console.info('welcome ' + user.name))
    .map(() => go(['/welcome']));

}

Call other actions/Effects before the final action as needed. Off course, this example works using '@ngrx/router-store'.

Read the amazing example-app code from the official repo.