Functional Guards in Angular

When Angular 14 came out, a lot changed with the Angular ecosystem. Now with Angular 16, even more, will be changing. With all these changes, I wanted to talk about one important change, particularly functional router guards. Angular 14 deprecated class-based router guards. We can still use them, but they will be going away soon. 

What is a Router Guard?

In Angular, a router guard is a class you can run in between navigation. It’s a mechanism that lets you determine if the current user has the correct permissions to view the next route. 

This was honestly one of my favorite features of the Angular framework. Let’s look at an example of the old guard

// auth.guard.ts
import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivate,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth/auth.service';

const userIsLoggedIn = true;

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(private auth: AuthService) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | boolean
    | UrlTree {
    return this.auth.userIsLoggedIn();
  }
}

This would be an example of what a classic class-based guard looks like. It gathers the route state, and you can return a boolean or a UrlTree as a plain value, a promise, or an Observable.

While our app will compile and function properly with this guard, you’ll notice the deprecation notice.

This gives us a pretty good idea of what’s to come. Eventually, they will be done away with altogether.

The New Guard

So why are we seeing the deprecation of class-based guards? Well, it’s to help make applications more streamlined. What does that mean exactly? Basically, they’re easier to write and maintain as well as share around your codebase. Let’s look at the same example rewritten.

One of the cool things is that we can write functional guards directly in our route. Immediately, you can see a big difference. This does the same thing but with a MUCH smaller footprint. With functional guards you can still inject services you might need with the inject function. 

We are able to write these to be extremely lean, but we still have access to the same things as a class-based component.

We can still put the next route and the router state as parameters in the argument, but we don’t need them. 

Sharing the guard

Checking if a user is logged in is a pretty generalized task. It’s likely something we’d want to share throughout our application so let’s put this function into its own file.

// auth.guard.ts
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from './services/auth/auth.service';

export const isUserLoggedInGuard = (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot
) => {
  const auth = inject(AuthService);
  return auth.userIsLoggedIn();
};

Now it’s easy to add to any route you want.

const routes: Routes = [
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full',
  },
  {
    path: 'home',
    canActivate: [isUserLoggedInGuard],
    loadComponent: () =>
      import('./home/home.component').then((m) => m.HomeComponent),
  },
];

The wrap-up

If you have an Angular app that uses class-based router guards I’d encourage you to start converting those. The effort is minimal, but it’ll save you a lot of headaches when it comes to upgrading to future versions of Angular.

Functional guards have a much smaller footprint than class-based guards, they’re easier to write, and they’re a lot more flexible. All in all, I think this is a win for the Angular ecosystem.

Previous
Previous

Standalone Components: Simplifying Angular

Next
Next

Angular 16: Required Inputs