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:
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
- Replace class implementation with functional approach
- Use
inject()
for dependencies - Return observables with proper redirection handling
Converted auth guard:
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
- Functional approach: Converted to constants with function signatures
- Dependency injection: Using
inject()
replaces constructor dependencies - Return types: Maintains same observable return type as original
- Navigation: Use
createUrlTree()
instead ofnavigate()
- Separation: Child guard simply calls parent function
Implementation in Routes
{
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:
@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:
@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:
export namespace AuthGuard {
export const canActivate = (route, state) => {
// Guard implementation
};
}
// Route usage
{ path: 'admin', canActivate: [AuthGuard.canActivate] }
Best Practices
URL trees: Use
router.createUrlTree()
instead ofrouter.navigate()
in guardsWhy?
Direct navigation in guards creates impure side effects and breaks during server-side rendering
Singular services: Use
inject()
at the guard level to avoid redundant injectionsLazy loading: Functional guards enable better code-splitting and tree-shaking
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
Approach | Best Case | Maintenance Impact |
---|---|---|
Direct Functional Guard | New projects or simple guards | ⭐ Lowest |
Class Conversion | Large legacy codebases | ⭐⭐ Medium |
Service-Based | Complex/reusable guard logic | ⭐⭐⭐ Highest |
Migration Summary
Key Takeaways
- Functional guards should be your default in Angular v15.2+
- Convert dependencies:
constructor
→inject()
- Replace
router.navigate()
withcreateUrlTree()
- Class guards are restored in v18 but functional remains preferred