1

I've built a Store interface:

export interface IStore {
  user: IUser;
}

where IUser is:

export interface IUser {
    id: string;
    name: string;
    username: string;
    customer: string;
}

In my component I create a subscription to IStore.user:

export class LoginComponent implements OnInit {

    private user$: Observable<IUser>;
    private userSub: Subscription;

    constructor(private store$: Store<IStore>)
    {
        this.user$ = this.store$.select(state => state.user);
    }

    ngOnInit():void {
        this.userSub = this.user$.subscribe(
            (user: IUser) => {
                this.router.navigate(['/app']);  ((((1))))
            },
            (error: any) => {
                this.addAlert(error.message);
            }
        );
    }

    ngOnDestroy():void {
        this.userSub.unsubscribe();
    }

    public login():void {
        this.store$.dispatch({ type: 'USER_REDUCER_USER_LOGIN' });
    }
}

Currently, ((((1)))) code is reached inmediatly subscription is just built. Nevertheless, the desired behavior is to reach ((((1)))) callback when 'USER_REDUCER_USER_LOGIN' action is dispatched inlogin()` method.

This is my UserReducer.ts:

export class UserReducer {
  private static reducerName = 'USER_REDUCER';

  public static reducer(user = initialUserState(), {type, payload}: Action) {
    if (typeof UserReducer.mapActionsToMethod[type] === 'undefined') {
      return user;
    }

    return UserReducer.mapActionsToMethod[type](user, type, payload);
  }

    // tslint:disable-next-line:member-ordering
    /**
     * Default reducer type. I want all sources.
     */
    public static USER_LOGIN = `${UserReducer.reducerName}_USER_LOGIN`;

    /**
     * User login success.
     */
    public static USER_LOGIN_SUCCESS = `${UserReducer.reducerName}_USER_LOGIN_SUCCESS`;
    private static userLoginSuccess(sourcesRdx, type, payload) {
        return Object.assign(<IUser>{}, sourcesRdx, payload);
    }

    /**
     * User login fails.
     */
    public static USER_LOGIN_FAILED = `${UserReducer.reducerName}_USER_LOGIN_FAILED`;
    private static userLoginFailed(sourcesRdx, type, payload) {
        return Object.assign(<IUser>{}, sourcesRdx, payload);
    }

  // ---------------------------------------------------------------

  // tslint:disable-next-line:member-ordering
  private static mapActionsToMethod = {
      [UserReducer.USER_LOGIN_SUCCESS]: UserReducer.userLoginSuccess,
      [UserReducer.USER_LOGIN_FAILED]: UserReducer.userLoginFailed,
  };
}

and initialUserState() is:

export function initialUserState(): IUser {
    return {
        id: '',
        name: '',
        username: '',
        customer: ''
    };
};

Any ideas?

EDIT

@Injectable()
export class UserEffects {
  constructor(
    private _actions$: Actions,
    private _store$: Store<IStore>,
    private _userService: UsersService,
  ) { }

  @Effect({ dispatch: true }) userLogin$: Observable<Action> = this._actions$
    .ofType('USER_REDUCER_USER_LOGIN')
    .switchMap((action: Action) =>
      this._userService.checkPasswd(action.payload.username, action.payload.password)
        .map((user: any) => {
          return { type: 'USER_REDUCER_USER_LOGIN_SUCCESS', payload: user };
        })
        .catch((err) => {
          return Observable.of({
            type: 'USER_REDUCER_USER_LOGIN_ERROR',
            payload: { error: err }
          });
        })
    );
}
Jordi
  • 20,868
  • 39
  • 149
  • 333

1 Answers1

1

What you are seeing is the expected behaviour. When you subscribe to the store, it immediately emits its current state and if it is yet to receive an action, that state will be the initial state.

The simplest solution would be to use the filter operator:

import 'rxjs/add/operator/filter';

ngOnInit():void {
  this.userSub = this.user$
    .filter(Boolean)
    .subscribe(
      (user: IUser) => { this.router.navigate(['/app']); },
      (error: any) => { this.addAlert(error.message); }
    );
}

With the filter applied, the user$ observable won't emit empty values, so the navigation won't occur until the user is logged in.

You can simplify the implementation further using the first operator. Subscribers are automatically unsubscribed when an observable completes or error. By using the first operator the composed observable will complete after the first emission, so manual unsubscription is not necessary and the userSub property and the ngOnDestroy method can be removed:

import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';

ngOnInit(): void {
  this.user$
    .filter(Boolean)
    .first()
    .subscribe(
      (user: IUser) => { this.router.navigate(['/app']); },
      (error: any) => { this.addAlert(error.message); }
    );
}

Another solution would be to use @ngrx/effects to manage the routing. You might decide that the LoginComponent's responsibility is to obtain the credentials and authenticate the user, but that it might not be that component's responsibility to decide where to navigate. If you wanted something else to be responsible for the navigation, you could create a routing effect that listened for USER_REDUCER_USER_LOGIN_SUCCESS.

You might also want to consider using the @ngrx/router-store to include the router state in your store - to facilitate time-travel debugging and the undoing of navigation changes. See this answer for more information.

Community
  • 1
  • 1
cartant
  • 57,105
  • 17
  • 163
  • 197
  • @Thanks a lot @cartant. In fact, I found out that using `@ngrx/effects` this behavior doesn't happen. I'd like to know why? I've don't edit any code despite of adding @Effect (see on post)... – Jordi Feb 09 '17 at 14:37