Overview
With the new Angular feature "functional guards and resolvers", it's easier to make sure that your route guards execute in order instead of all at once.
When there are multiple guards in a route configuration, they are normally executed all at the same time. For example, let's say you have the following route configuration:
{
path: 'crisis-center',
canActivate: [FirstGuard, SecondGuard],
...
}
Even though the guards are in an array, SecondGuard
will always execute no matter whether FirstGuard
returns true
or not. What we would like to achieve instead is to have SecondGuard
not fire at all if FirstGuard
doesn't allow it.
For a more life-like example, imagine that your first guard makes sure the user is properly logged in, while the second guard already assumes that and proceeds to call an API that works only for logged-in users.
Functional solution
Up to recent times, the best way to achieve guard ordering was to write an extra "parent" guard to call the other guards in order. This meant that you had to create an additional ordering guard class for every configuration of ordered guards, or to write your guard in a more generic way that made use of the router's data
object.
Now there's a much simpler way. If you search for "Functional router guards" in the Angular 15 announcement blog post, you'll see a short description of a new Angular feature. What it says is that you can now use functions instead of classes wherever you want to put a route guard.
In the simplest form, it means we can write the following in the route configuration:
canActivate: [() => isUserLoggedIn()]
If we combine this new feature with the recently-updated inject
function, we could replace any guard class as follows:
canActivate: [FirstGuard]
with:
canActivate: [(route, state) =>
inject(FirstGuard).canActivate(route, state)]
While this is more verbose, it gives us a lot of power to, e.g., combine guards.
Ordered synchronous guards
For the first case, imagine that all your guards are synchronous and they immediately return a boolean. If you want to make them execute in order, you could write the following:
const orderedSyncGuards =
(guards) =>
(route, state) =>
guards.every(guard =>
inject(guard).canActivate(route, state));
const ROUTE = {
...
canActivate: [orderedSyncGuards([FirstGuard, SecondGuard])]
This example is a bit simplistic, and I removed types of orderedSyncGuards
to make the code more readable. Let's now go for a full-blown example with asynchronous guards that can also return UrlTrees
.
Ordered asynchronous guards
However, before I go there, a short disclaimer: executing multiple async guards in order will make your page load slower because later guards won't even start their requests until earlier guards finish. If you value performance, you are better off rewriting your guards to handle unfavorable conditions instead. The solution below is provided for the sake of completeness.
You can follow the full example on StackBlitz.
This time we'll assume that all guards return observables that emit either a boolean or a UrlTree
and then complete.
Because we want to run the guards one after another, we can use concatMap
as our main operator. We also want to break out of the function if the value returned by any guard is different than true
, and we want to return the last emitted value.
After some fun with TypeScript types, the following is the final outcome:
interface AsyncGuard extends CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree>;
}
function orderedAsyncGuards(
guards: Array<new () => AsyncGuard>
): CanActivateFn {
return (route, state) => {
// Instantiate all guards.
const guardInstances = guards.map(inject) as AsyncGuard[];
// Convert an array into an observable.
return from(guardInstances).pipe(
// For each guard, fire canActivate and wait for it
// to complete.
concatMap((guard) => guard.canActivate(route, state)),
// Don't execute the next guard if the current guard's
// result is not true.
takeWhile((value) => value === true, /* inclusive */ true),
// Return the last guard's result.
last()
);
};
}
const ROUTE = {
...
canActivate: [orderedAsyncGuards([FirstGuard, SecondGuard])]
And that's how you can write a universal sync or async guard ordering, using a single function call!
I will leave writing a universal ordering guard (sync/async, boolean/UrlTree) as an exercise for the reader :) Here's a hint: start with the async solution and inspect the result of guard.canActivate
call.
Top comments (3)
This is neat! I've built upon it so that you can give it an array of functional guards - the key part being running each one in the current injection context
Thank you! This works for me too. I wrote some jasmine unit tests for it.
this works for me, thanks!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.