I'm using Angular (v20) with Signals and httpResource from @angular/common/http to manage my user's state and authentication. My AuthService exposes a currentUserResource that loads user information and a computed isLoggedin signal.
The challenge is implementing an authGuard (CanActivateFn) that needs to wait for the initial authentication status to be known (i.e., for currentUserResource to complete its initial load) before allowing or denying access to a route. I'm looking for the cleanest, most idiomatic "Angular Signals" way to do this, ideally avoiding RxJS.
Here's a simplified version of my AuthService:
import { Injectable, computed, inject } from '@angular/core';
import { httpResource, HttpResourceRef } from '@angular/common/http';
import { User } from '../interfaces/user.interface';
import { Router } from '@angular/router';
const API_URL = '/api/auth';
@Injectable({ providedIn: 'root' })
export class AuthService {
private router = inject(Router);
currentUserResource: HttpResourceRef<User | null> = httpResource<User | null>(() => ({
url: `${API_URL}/current`, // Endpoint to get the current user
defaultValue: null,
}));
public readonly isLoggedIn = computed(() => !!this.currentUserResource());
public readonly currentUser = computed(() => this.currentUserResource());
}
And here's the skeleton of my functional authGuard:
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard: CanActivateFn = async (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.currentUserResource.isLoading()) {
// What's the best approach here to wait?
// 1. Use toObservable + firstValueFrom (RxJS)?
// 2. Manually create a Promise with an Angular `effect` and resolve it?
// 3. Is there another Angular-recommended method or built-in utility?
await /* ??? */; // This is where I need the solution
}
// Once loading is complete (isLoading is false):
if (!authService.isLoggedIn()) {
router.navigate(['/login']);
return false;
}
return true;
};
Right now I have this approach, but I'm not satisfied with the usage of an effect:
export const authGuard: CanActivateFn = async (route, state) => {
const authService = inject(AuthDataClient);
const router = inject(Router);
if (authService.currentUserResource.isLoading()) {
await new Promise<void>((resolve) => {
const effectRef = effect(() => {
if (!authService.currentUserResource.isLoading()) {
resolve();
effectRef.destroy();
}
});
});
}
const isLoggedIn = authService.isLoggedin();
if (!isLoggedIn) {
router.navigateByUrl('/signin');
return false;
}
return true;
};
I'm really looking for the new "signals-way", most modern and recommended approach to this issue.
In order to make the canActivateFn
wait for a particular state, we have to return either a promise
or an Observable
.
The signals are generally synchronous (cannot be used to make the can activate wait unless we wrap it inside an asynchronous context like promise or Observable)
, so as far as I know, we need an Observable
or promise
wrapper on top of the signal, to allow the can activate function to wait.
export const authGuard: CanActivateFn = async (route, state) => {
const authService = inject(AuthDataClient);
const router = inject(Router);
return toObservable(authService.currentUserResource.isLoading()).pipe(
skipUntil((isLoading: boolean) => isLoading),
take(1),
map(() => {
const isLoggedIn = authService.isLoggedin();
if (!isLoggedIn) {
return router.createUrlTree(['/login']);
}
return true;
});
);
};
export const authGuard: CanActivateFn = async (route, state) => {
const authService = inject(AuthDataClient);
const router = inject(Router);
await new Promise<void>((resolve) => {
const effectRef = effect(() => {
if (!authService.currentUserResource.isLoading()) {
effectRef.destroy();
const isLoggedIn = authService.isLoggedin();
if (!isLoggedIn) {
return router.createUrlTree(['/login']);
}
resolve();
}
}, {manualCleanup: true});
});
};