1

How to wait for the completion of an Effect in NgRx. I am stuck in this situation. I have an effect that loads a token from the storage and the dispatch an action to update the Store depending that the token was found or not.

autoLogin$ = createEffect(() => this.actions$.pipe(
    ofType(AuthActions.startAutoLogin),
    switchMap(action => {
      return from(AuthUtils.getTokenFromStorage(USER_TOKEN_STORAGE_KEY));
    }),
    switchMap(token => {
      if (AuthUtils.isProvidedTokenNotExpired(token)) {
        return from(AuthUtils.getTokenData(token));
      }
      return EMPTY;
    }),
    map(tokenData => {
      if (tokenData) {
        return AuthActions.autoLogin({payload: tokenData});
      }
    })
  ));

The action AuthActions.autoLogin({payload: tokenData}) is dispatched and used to update one field in the store:

export interface AuthState {
  isLoggedIn: boolean,
}

Finally I use that field in a Router Guard to check if the user is logged in or not:

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanLoad, CanActivate {

  constructor(private router: Router,
              private store: Store<fromAuth.AuthState>) {
  }

 canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
     
  return this.store.select(AuthSelectors.selectIsLoggedIn).pipe(
      tap(isLoggedId => {
        if (!isLoggedId) {
          this.router.navigateByUrl('/login').then();
        }
      }),
    );
  }
}

The problem is:

The Router Guard is executed before the Effect, the Store is updated too late and user is redirected to the login page. When I look at the state of the store after the redirection, isLoggedIn is true.

Is it possible in the Router Guard to wait the end of the Effect execution redirecting or not?

akuma8
  • 4,160
  • 5
  • 46
  • 82
  • Not really. The trick is to use an additional key there (like loaded: true | false) that you can swap using the same (success) action. Then in your guard you can switchmap the loaded selector to the loggedIn selector. Does that make sense? Careful though, using selectors for guards can be tricky as they always initially emit the current value in the store (which might be null or undefined. – MikeOne Mar 30 '22 at 21:21
  • Thanks for the suggestion, I’ll give a try. I could use the logic defined in the Effect inside the Guard but I don’t want to load the token there. Regarding the selector in the Guard, the first is `false` and that’s why I am getting redirection every time – akuma8 Mar 30 '22 at 21:37
  • False would only be the real value if loaded is true. That’s kinda the trick. – MikeOne Mar 30 '22 at 21:52
  • It doesn’t solve my issue if the emitted value is always false – akuma8 Mar 30 '22 at 22:14
  • Why would it be..? You’ll have to make sure your observable in the guard only emits when loaded is true. You can filter for that. – MikeOne Mar 30 '22 at 22:27
  • The filter will block forever if loaded=false and the guard will not redirect to the login page – akuma8 Mar 30 '22 at 22:43
  • You don't select `isLoggedIn`, you select `loaded` and wait for it to emit true – Will Alexander Apr 09 '22 at 20:40

1 Answers1

0

I had the same behaviour and managed to fix it.

What I did was to add the isLoginResolved to the state. After a successful or failed login/auto-login I set the property to true , because the login/auto-login happened.

In the guard I'm doing the auto-login part. What's happening there is that I'm checking if the user is not logged in or there was no login/auto-login attempt then it will try to auto-login the user; in the last part I'm waiting for the isLoginResolved to be true.

user.state.ts

export interface UserState 
  {
  isLoginResolved: boolean;
  userDetails: Player | null;
  token: string;
  authError: any;
  }

auth.guard.ts

 canActivate(
       route: ActivatedRouteSnapshot,
       state: RouterStateSnapshot
   ): boolean | Observable<boolean> | Promise<boolean> {
       return this.authService.getIsAuth().pipe(
           tap((isAuthenticated: boolean) => {
               if (!isAuthenticated) {
                   this.router.navigate(["../"]).then();
               }
           })
       );
   }

auth.service.ts

   getIsAuth() {
       const isAuthenticated$: Observable<boolean> = this.store.select(isAuthenticated);
       const isLoginResolved$: Observable<boolean> = this.store.select(selectUserResolved);

       zip(isLoginResolved$, isAuthenticated$)
           .pipe(map(([isLoginResolved, isAuthenticated]) => ({ >isLoginResolved, isAuthenticated })))
           .pipe(first())
           .subscribe((x) => {
               if (!x.isAuthenticated || !x.isLoginResolved) {
                   this.store.dispatch(autoLogin());
               }
           });

       return this.store
           .select(selectUserResolved)
           .pipe(filter((resolved) => resolved))
          .pipe(map(() => isAuthenticated$))
     }    
Jeff Schaller
  • 2,352
  • 5
  • 23
  • 38