22

Does the Angular router have any restrictions to be used inside an NgRx effect?

I just started learning NgRx and I have the following code:

@Effect() public authenticate$ = this.actions$
    .ofType(authenticationActions.AUTHENTICATE)
        .switchMap((action: AuthenticateAction) => this.authenticationService.authenticate(action.payload)
            .map((data: TokenData) => {
                const user: User = {
                    token: data.token,
                    username: 'dummy',
                };
                console.log(data);
                this.router.navigateByUrl('/');
                return new authenticationActions.AuthenticateSuccessAction(user);
            })
            .catch(error => { console.log(error); return Observable.throw(error); })
        );

The console logs the data variable and the AuthenticateSuccessAction action is being triggered, so the router line is being executed but the navigation doesn't happen.

dhilt
  • 18,707
  • 8
  • 70
  • 85
rbasniak
  • 4,484
  • 11
  • 51
  • 100
  • This should work normally, maybe try a different URL to make sure that its not a bug before you're navigating to root? `this.router.navigateByUrl('/login')`; Ensure you're not using something like an AuthGuard that redirects you back to your login screen. – Joshua Chan May 28 '18 at 14:40
  • I already tried different urls and also disabled all guards and created a test button in the login page to use that same statement and it navigates. The problem is that statement inside the effect. This is very very strange – rbasniak May 28 '18 at 16:49
  • I have similar implementation as you and am able to route my app without problem. This is silly but, make sure you've injected the router in your constructor. – Joshua Chan May 28 '18 at 18:24
  • 1
    do the navigation within `tap` operator (in older versions of rxjs it was called `do`). – dee zg May 29 '18 at 16:58

4 Answers4

29
@Effect() public authenticate$ = this.actions$.pipe(
    ofType(authenticationActions.AUTHENTICATE),
     map(action => action.payload),
    exhaustMap((auth: any) => 
      this.authenticationService.authenticate(auth)
        .map((data: TokenData) => {
            return user: User = {
                token: data.token,
                username: 'dummy',
            };
        }).catch(error => { console.log(error); return Observable.throw(error); 
       }).pipe(
          map(user =>new authenticationActions.AuthenticateSuccessAction(user))
        )
    );)

  @Effect({ dispatch: false })
   loginSuccess$ = this.actions$.pipe(
     ofType(authenticationActions.AuthenticateSuccessAction),
     tap(() => this.router.navigate(['/']))
   );

Use exhaustMap and when you dispatching 'AuthenticateSuccessAction' action, do another effect for redirecting.

Personally, I like to separate all the services from effects, then you can use catchError() operator after success login for dispatching another action in case of failure login.

hope this works. PS: I did not verify this answer but logic is like this.

m.akbari
  • 572
  • 8
  • 22
13

There should be an approach allowing not to create separate effect for redirect, something like

this.actions$.pipe(
  ofType(authenticationActions.AUTHENTICATE),
  switchMap(action =>
    this.authenticationService.authenticate(action.payload).pipe(
      map(data => new authenticationActions.successAction(data)),
      tap(() => this.router.navigate(['/'])),
      catchError(error => new authenticationActions.failAction(error))
    )
);

The thing is that tap will not be invoked if the service call failed, both map and tap will be skipped in favor of catchError if the failure case.

dhilt
  • 18,707
  • 8
  • 70
  • 85
  • From angular 11, @Effect() is deprecated ! You have to use creatEffect instead : see https://ngrx.io/guide/migration/v11 at the bottom of this page : you will find all explanation. – Vifier Lockla Dec 23 '22 at 14:08
5

following the response of m.akbari, in the case they use the actions differently, the "dispatch : false" goes to the last.

the key is the "dispatch : false", if this is not indicated an infinite loop is generated

 loginNavigate$ = createEffect(
    () => this.actions$.pipe(
        ofType(usuariosActions.loginUsuarioSuccess),
        tap(() => {
            console.log('iniciando la navegacion');

            this.router.navigate(['/']);
        })
    ), { dispatch: false }
);
0

When using this.router.navigate() in combination with @ngrx/router-store effects, there a few things to consider when handling navigation:

  1. Use mergeMap instead of tap because this.router.navigate() returns a promise and tap doesn't allow asynchronous operations to be resolved or awaited.

  2. { dispatch: false } indicates that this effect does not dispatch any new action.

  3. Use catchError to handle errors that may occur during navigating. If using catchError do not include { dispatch: false } in the effect options since you will be dispatching an action to handle the error.

Example:

navigateTo$ = createEffect(
  () => {
    return this.actions$.pipe(
      ofType(myActions.navigateTo),
      mergeMap(({ path }) => {
        return this.router.navigate([path]);
      })
    );
  },
  { dispatch: false }
);

To handle errors, we can modify the code as follows:

navigateTo$ = createEffect(
  () => {
    //...
      catchError(error => of(myActions.doSomething())
    //...
  },
  // { dispatch: false }
);
krema
  • 939
  • 7
  • 20