DEV Community

Dzinx
Dzinx

Posted on

A New Way Of Ordering Guards In Angular

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],
    ...
}
Enter fullscreen mode Exit fullscreen mode

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()]
Enter fullscreen mode Exit fullscreen mode

If we combine this new feature with the recently-updated inject function, we could replace any guard class as follows:

  canActivate: [FirstGuard]
Enter fullscreen mode Exit fullscreen mode

with:

  canActivate: [(route, state) =>
    inject(FirstGuard).canActivate(route, state)]
Enter fullscreen mode Exit fullscreen mode

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])]
Enter fullscreen mode Exit fullscreen mode

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])]
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
garethrpratt profile image
garethrpratt • Edited

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

import { Injector, inject, runInInjectionContext } from '@angular/core';
import { CanActivateFn } from '@angular/router';
import { Observable, from, of } from 'rxjs';
import { concatMap, last, takeWhile } from 'rxjs/operators';

export function SequentialGuards(guards: CanActivateFn[]): CanActivateFn {
  return (route, state) => {
    const injectionContext = inject(Injector);
    // Convert an array into an observable.
    return from(guards).pipe(
      // For each guard, fire canActivate and wait for it to complete.
      concatMap((guard) => {
        return runInInjectionContext(injectionContext, () => {
          var guardResult = guard(route, state);
          if (guardResult instanceof Observable) {
            return guardResult;
          } else if (guardResult instanceof Promise) {
            return from(guardResult);
          } else {
            return of(guardResult);
          }
        });
      }),
      // Don't execute the next guard if the current guard's result is not true.
      takeWhile((value) => value === true, true),
      // Return the last guard's result.
      last()
    );
  };
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jrueteo profile image
Jrue Teo

Certainly! To order guards in Angular, you can use the canActivate property in your route configuration. Here's an example with three lines of code:

const routes: Routes = [
{ path: 'example', component: ExampleComponent, canActivate: [AuthGuard1, AuthGuard2] },
// Add more routes as needed
];
In this example, AuthGuard1 and AuthGuard2 are the guards, and they will be executed in the order specified in the array. Adjust the guards and route configurations based on your application's requirements.

Collapse
 
wakiy profile image
wakiy

I found this other thread with similar implementation as mine. It worked for them and they're returning an Observable just as I am. I'm not sure why it isn't working in dude theft wars mod menu multiplayer100% correctly. I'll give your solution a try. Thanks!

Some comments may only be visible to logged-in visitors. Sign in to view all comments.