Skip to content

Angular CanActivate: Migration to Functional Guards

Problem

Angular deprecated the CanActivate and CanActivateChild interfaces starting in v15.2. Developers began seeing deprecation warnings when using class-based guards like this:

typescript
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthenticationService) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.authService.checkLogin().pipe(
      map(() => true),
      catchError(() => {
        this.router.navigate(['route-to-fallback-page']);
        return of(false);
      })
    );
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.canActivate(route, state);
  }
}

The Angular documentation explicitly recommends moving to pure JavaScript functions instead of class-based guards.

Note on Angular v18 update: CanActivate was un-deprecated in Angular v18 (PR #56408) for backward compatibility. However, functional guards remain the recommended approach.

Solution: Converting to Functional Guards

Functional guards are simpler, more tree-shakable, and align with Angular's modern patterns. Use dependency injection via inject() to access services.

Step-by-Step Migration

  1. Replace class implementation with functional approach
  2. Use inject() for dependencies
  3. Return observables with proper redirection handling

Converted auth guard:

typescript
import { CanActivateFn, CanActivateChildFn } from '@angular/router';
import { inject } from '@angular/core';
import { AuthenticationService } from './authentication.service';
import { catchError, map } from 'rxjs/operators';
import { Router } from '@angular/router';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthenticationService);
  const router = inject(Router);

  return authService.checkLogin().pipe(
    map(() => true),
    catchError(() => {
      // Prefer createUrlTree for navigation in guards
      return router.createUrlTree(['route-to-fallback-page']);
    })
  );
};

export const authGuardChild: CanActivateChildFn = (route, state) => 
  authGuard(route, state);

Key Changes

  1. Functional approach: Converted to constants with function signatures
  2. Dependency injection: Using inject() replaces constructor dependencies
  3. Return types: Maintains same observable return type as original
  4. Navigation: Use createUrlTree() instead of navigate()
  5. Separation: Child guard simply calls parent function

Implementation in Routes

typescript
{
  path: 'protected',
  canActivate: [authGuard],
  canActivateChild: [authGuardChild],
  children: [
    { path: 'dashboard', component: DashboardComponent }
  ]
}

Alternative Approaches

Option 1: Helper Functions for Class Guards

Angular provides functions to convert classes to functional guards:

typescript
@Injectable({ providedIn: 'root' })
export class AdminGuard {
  canActivate() {
    // Guard logic
  }
}

const routes: Routes = [{
  path: 'admin',
  canActivate: mapToCanActivate([AdminGuard])
}];

Option 2: Service-Based Pattern

If you prefer class-based logic:

typescript
@Injectable({ providedIn: 'root'})
class PermissionsService {
  canActivate() {
    // Authorization logic
  }
}

export const authGuard: CanActivateFn = () => 
  inject(PermissionsService).canActivate();

Option 3: Static Class Methods

For grouping related guard functions in a namespace:

typescript
export namespace AuthGuard {
  export const canActivate = (route, state) => {
    // Guard implementation
  };
}

// Route usage
{ path: 'admin', canActivate: [AuthGuard.canActivate] }

Best Practices

  1. URL trees: Use router.createUrlTree() instead of router.navigate() in guards

    Why?

    Direct navigation in guards creates impure side effects and breaks during server-side rendering

  2. Singular services: Use inject() at the guard level to avoid redundant injections

  3. Lazy loading: Functional guards enable better code-splitting and tree-shaking

  4. Testing patterns: Simplify guard testing with standalone functions:

    typescript
    // Test setup
    TestBed.runInInjectionContext(() => {
      const result = authGuard(route, state);
      // Assert on result
    });

When to Use Each Approach

ApproachBest CaseMaintenance Impact
Direct Functional GuardNew projects or simple guards⭐ Lowest
Class ConversionLarge legacy codebases⭐⭐ Medium
Service-BasedComplex/reusable guard logic⭐⭐⭐ Highest

Migration Summary

Key Takeaways

  • Functional guards should be your default in Angular v15.2+
  • Convert dependencies: constructorinject()
  • Replace router.navigate() with createUrlTree()
  • Class guards are restored in v18 but functional remains preferred