Les guards décident si vous pouvez entrer (canActivate), si la route existe pour vous (canMatch) ou si vous pouvez partir (canDeactivate). Les resolvers chargent des données avant l’affichage de la page. Dans l’Angular moderne, tous sont des fonctions qui utilisent inject() — pas de classes, pas d’interfaces à implémenter.
Un guard fonctionnel
import { inject } from '@angular/core';
import { CanActivateFn, RedirectCommand, Router } from '@angular/router';
export const adminGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthStore);
const router = inject(Router);
if (auth.isAdmin()) return true;
// UrlTree : redirection simple
// return router.parseUrl('/login');
// RedirectCommand : redirection + options de navigation
return new RedirectCommand(router.parseUrl(`/login?returnUrl=${state.url}`), { replaceUrl: true });
};Zone publique. Tout le monde peut voir cette page.
canDeactivate : modifications non enregistrées
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) =>
!component.hasUnsavedChanges() || confirm('Vous avez des modifications non enregistrées. Quitter quand même ?');
@Component({ /* ... */ })
export class EditProfilePage implements HasUnsavedChanges {
readonly form = form(this.model);
hasUnsavedChanges() {
return this.form().dirty();
}
}Resolvers
export const reportResolver: ResolveFn<Report> = (route) => {
const api = inject(ReportsApi);
return api.get(Number(route.paramMap.get('id'))); // Observable / Promise / valeur simple
};
@Component({ /* ... */ })
export default class ReportPage {
readonly report = input.required<Report>(); // vient de resolve, déjà chargé
}Resolver httpResource dans le composant
─────────────────────────────────────────────────────────────────────────────
la page apparaît avec les données prêtes la page apparaît tout de suite, avec un chargement
la navigation « attend » les données la navigation est instantanée
erreur = navigation annulée erreur = état dans l’UI (error())
adapté à : petites données requises adapté à : listes, dashboards, UX rapidecanMatch ou canActivate
// canMatch vs canActivate
// canMatch : décide si la route « existe » pour l’utilisateur. Sinon → Angular essaie la route suivante
// avec le même chemin, et le chunk lazy n’est PAS téléchargé.
// canActivate : la route existe, mais l’entrée est bloquée (le chunk est peut-être déjà téléchargé).
const routes: Routes = [
{ path: 'dashboard', canMatch: [isAdmin], loadComponent: () => import('./admin-dashboard') },
{ path: 'dashboard', loadComponent: () => import('./user-dashboard') }, // repli pour tous les autres
];
// v22: currentSnapshot est désormais un paramètre obligatoire dans CanMatchFn / CanMatch.canMatch (route, segments, currentSnapshot)
// v22.2: vous pouvez LANCER un RedirectCommand depuis les guards/resolvers pour redirigerL’ordre d’exécution
Pendant une navigation, le routeur passe par des étapes fixes. Si une étape dit « non », les suivantes ne s’exécutent pas :
canMatch, pendant la recherche de la route. Un « non » ici signifie que la route n’existe pas pour cet utilisateur, et le routeur essaie la suivante.canDeactivatesur les routes que vous quittez.canActivateChildetcanActivate, du parent vers l’enfant.- Les resolvers. Le composant n’apparaît qu’une fois qu’ils ont tous terminé.
Un guard asynchrone
Un guard peut renvoyer une Promise ou un Observable, et la navigation attend. Le cas typique : l’utilisateur n’est pas encore chargé au démarrage de l’app. Attendez la première vraie valeur, pas l’état vide initial.
export const authGuard: CanActivateFn = (_route, state) => {
const auth = inject(AuthStore);
const router = inject(Router);
// attendre que ce ne soit plus « loading », puis décider une seule fois
return toObservable(auth.status).pipe(
filter((s) => s !== 'loading'),
take(1),
map((s) =>
s === 'signed-in'
? true
: router.createUrlTree(['/login'], { queryParams: { next: state.url } }),
),
);
};Les données du resolver comme input
Avec withComponentInputBinding(), les clés de resolve arrivent directement dans les inputs du composant, comme les paramètres d’URL. Le composant n’a plus besoin d’ActivatedRoute.
// app.config.ts
provideRouter(routes, withComponentInputBinding());
// routes.ts
{ path: 'reports/:id', component: ReportPage, resolve: { report: reportResolver } }
// report-page.ts — sans ActivatedRoute
export class ReportPage {
readonly id = input.required<string>(); // depuis l’URL
readonly report = input.required<Report>(); // depuis resolve
}Quand OUI, quand NON
canMatchsur les fonctionnalités réservées (admin) — vous ne téléchargez pas le code pour rien.canActivatepour l’authentification / les permissions sur les pages.canDeactivatepour les formulaires avec des modifications non enregistrées.- Un resolver pour des données petites et indispensables, sans lesquelles la page n’a pas de sens.
- Les guards comme seule sécurité — le serveur doit vérifier de toute façon.
- Des resolvers pour de grandes listes → la page semble « figée ». Utilisez
httpResource+ un skeleton. - De la logique métier dans un guard (calculs, transformations) → déplacez-la dans un service.
Pièges fréquents
- Un guard qui renvoie
falsesans redirection → l’utilisateur reste sur place sans comprendre pourquoi. Renvoyez unUrlTree/RedirectCommand. - Un guard sur le parent, mais des navigations entre enfants :
canActivatesur le parent ne se ré-exécute pas quand l’enfant change — utilisezcanActivateChildourunGuardsAndResolvers. - Un resolver qui échoue annule la navigation — gérez l’erreur (redirection vers une page d’erreur).
Aide-mémoire
// ordre : canMatch → canDeactivate → canActivateChild / canActivate → resolve
canMatch: [() => inject(AuthStore).isAdmin()]
canActivate: [authGuard]
canActivateChild: [authGuard]
canDeactivate: [(c: HasUnsavedChanges) => !c.hasUnsavedChanges() || confirm('Quitter sans enregistrer ?')]
resolve: { report: reportResolver }
// ce qu’un guard peut renvoyer
return true | false | router.createUrlTree(['/login']) | new RedirectCommand(tree)
return Promise<...> | Observable<...> // la navigation attend
// réexécuter les guards aussi quand les query params changent
runGuardsAndResolvers: 'paramsOrQueryParamsChange'