Functional Guards & Resolvers (v15)
📚 What are Functional Guards?
Functional guards replace class-based guards with simpler functions:
- CanActivateFn: Controls if route can be activated
- CanDeactivateFn: Controls if user can leave route
- CanMatchFn: Controls if route can be matched (replaces CanLoad)
- ResolveFn: Pre-fetches data before route activation
- inject() works: Use inject() inside functional guards
🎯 Interview Questions
- Q1: What is the advantage of functional guards over class-based guards?
- A: Less boilerplate, no need for classes, better tree-shaking, can use inject() for dependencies.
- Q2: How do you inject services in functional guards?
- A: Use inject() function inside the guard: const auth = inject(AuthService);
- Q3: What replaced CanLoad in Angular 15?
- A: CanMatchFn - controls whether route can be matched during navigation.
- Q4: How do you redirect in a functional guard?
- A: Return router.createUrlTree(['/path']) or router.parseUrl('/path').
🔧 Guard Types Demo
✅ CanActivateFn
Protects routes from unauthorized access
Simulated: User is authenticated ✓
⚠️ CanDeactivateFn
Warns before leaving with unsaved changes
🔄 ResolveFn
Pre-fetches data before route loads
Resolved user: John Doe (john@example.com)
🔄 Class vs Functional Guards
❌ Class-based (Old)
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private auth: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
if (this.auth.isLoggedIn()) {
return true;
}
return this.router.parseUrl('/login');
}
}✅ Functional (New)
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.parseUrl('/login');
};💻 Functional Guards Code
import { inject } from '@angular/core';
import {
CanActivateFn,
CanDeactivateFn,
CanMatchFn,
ResolveFn,
Router
} from '@angular/router';
// ===== CanActivateFn - Route Protection =====
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isAuthenticated()) {
return true;
}
// Redirect to login with return URL
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
};
// ===== CanDeactivateFn - Unsaved Changes =====
export const unsavedChangesGuard: CanDeactivateFn<{ hasChanges: () => boolean }> =
(component, currentRoute, currentState, nextState) => {
if (component.hasChanges()) {
return confirm('Discard unsaved changes?');
}
return true;
};
// ===== CanMatchFn - Feature Flags =====
export const featureGuard: CanMatchFn = (route, segments) => {
const features = inject(FeatureFlagService);
return features.isEnabled(route.data?.['feature']);
};
// ===== ResolveFn - Data Pre-fetching =====
export const userResolver: ResolveFn<User> = (route) => {
const userService = inject(UserService);
return userService.getUser(route.params['id']);
};
// ===== Using in Routes =====
export const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
resolve: { user: userResolver }
},
{
path: 'editor',
component: EditorComponent,
canDeactivate: [unsavedChangesGuard]
},
{
path: 'beta-feature',
loadComponent: () => import('./beta.component'),
canMatch: [featureGuard],
data: { feature: 'beta' }
}
];