When building a web application, chances are that you will have some routing involved at some points.
However, those routes might not be suitable for everyone. As an example, you might not want an anonymous user changing the profile page of someone else.
Angular is no different from the (many) other frameworks and has to solve this issue.
Hopefully, and thanks to its opinionated nature, those controls are baked-in into the framework. However, when getting started with them, it may be a bit obscure at first.
In this article we will see what guards are, how to use them and see some concrete use cases through various scenarios where route guards shine.
So, whether you're interested in restricting access to certain routes, preventing unauthorized actions, or ensuring a smooth user experience, this guide has got you covered. Let's unlock the full potential of Angular route guards and take your application's routing capabilities to new heights!
Table of Contents
- Understanding Route Guards
- Classes or Functions?
- The Different Types of Route Guards
- Anatomy of a Guard
- Combining Route Guards
- Inlined Guards
- Takeaways
Understanding Route Guards
Route guards are set of functions and classes that control and manage routing and navigation within your Angular application.
They provide a way to protect routes, enforce certain constraints such as authentication or perform other checks on specific routes.
As an example, some use cases that guards can help to manage could be:
- Preventing an anonymous user to see its profile page
- Preventing a user without any basket to reach the checkout page
- Preventing a regular user to access the administration panel
Classes or functions?
Not so long ago, guards where interfaces that needed to be implemented and registered in your modules.
However, since the latest versions of Angular, class-based guards are deprecated in favor of functional guards.
Instead of implementing an interface, it is now possible to use a function to achieve the same result.
Class-based guards can still be used and easily converted into functional ones, thanks to the
inject
function:const myGuard: CanActivateFn = inject(CanActivateMyGuard).canActivate;
There is even a PR that has been submitted to provide helper functions for that purpose.
The different types of route guards
As we previously mentioned it, there is a lot of different use cases for guards such as:
- Preventing a user to leave a page he has pending edits in
- Accessing unauthorized views
- Filtering logged in users
And much more.
Even if those use cases involves routing, the intention is different in each case, that's why the Angular guard's API will also exposes different signatures:
Let's break it down!
CanMatchFn
CanMatchFn
is a guard to be used in the context of lazy loading.
When evaluated, it will help Angular's router to determine whether or not the associated lazy-loaded route should be evaluated.
If false
, the bundle will not be loaded and the route will be skipped.
⚗ Use it when you want to evaluate if the user can load a lazy-loaded route.
🔭 Example
As a regular user, I should not be able to load the/admin
route.const routes: Route[] = [ { path: 'admin', loadChildren: () => import('./admin').then(m => m.AdminModule), canLoad: [AdminGuard] }, ];
CanActivateFn
CanActivateFn
may be the more intuitive route guard: it helps to determine whether or not the current user can navigate to the route it decorates.
There is no lazy-loading involved here, the route will be effectively loaded and evaluated by the router but its access could be prevented based on the guard's result.
⚗ Use it when you want to prevent a user to access a given route.
🔭 Example
As an anonymous user, I should not be able to see my/profile
page until I am authenticated.const routes: Route[] = [ { path: 'profile', component: ProfileComponent, canActivate: [authenticationGuard] }, ];
CanActivateChildFn
CanActivateChildFn
is the same concept as CanActivateFn
but applied to the children of the parent route.
Given a route, evaluating this guard will indicate the router if you can access any of its children.
⚗ Use it when you have a parent-children route hierarchy and you want to prevent access to those children but maybe not the parent
🔭 Example
As a coach, I can see my team's details on/team/:id
and edit it on/team/:id/edit
.
As a regular user, I can only see the team's details on/team/:id
but cannot access the edit page on/team/:id/edit
.const routes: Route[] = [ { path: 'team/:id', component: TeamDetailsComponent, canActivateChild: [teamCoachGuard], children: [ { path: 'edit', component: TeamEditComponent } ] }, ];
CanDeactivateFn
The CanDeactivateFn
is slightly different than the other guards: other guards tend to prevent you from reaching a route, but this one prevent you from leaving it.
There is no concerns about lazy loading here since the route is already loaded and activated.
⚗ Use it when you want to prevent a user from losing data potentially tedious to type again or break a multi-steps process.
🔭 Example
As a job candidate, I want to be prompted to confirm before navigating away from the page if my unsaved cover letter is not saved as a draft.const routes: Route[] = [ { path: 'online-application', component: OnlineApplicationComponent, canDeactivate: [unsavedChangesGuard] }, ];
Anatomy of a Guard
Guards vary in purpose, but mainly follow the same structure. Given a route, it will either return (synchronously or not):
- A
boolean
to indicate whether the route can be reached (true
if it can,false
otherwise) - Or an
UrlTree
designating a route to redirect the user to
The outputs are the same for all guards, however the input parameters may not. Before implementing the logic bound to a guard, check what its parameters are.
To understand it, let's write a profileGuard
which will:
- Redirect any anonymous user to
/login
- Reject navigation for any user that would access the profile page of someone else
- Grant navigation for the current user if he wants to browse his own profile page
const profileGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
):
| Observable<boolean | UrlTree>
| Promise<boolean | UrlTree>
| boolean
| UrlTree => {
const currentUser = inject(CurrentUserService).getCurrentUser();
// 👇 Redirects to another route
const isAnonymous = !currentUser;
if (isAnonymous) {
return inject(Router).createUrlTree(["/", "login"]);
}
const profilePageId = route.params["id"];
// 👇 Grants or deny access to this route
const attemptsToAccessItsOwnPage = currentUser.id === profilePageId;
return attemptsToAccessItsOwnPage;
};
Its usage is no different from our previous example:
const routes: Route[] = [
{
path: 'profile',
component: ProfileComponent,
canActivate: [profileGuard]
},
];
Combining Route Guards
We covered what guards are and their usages but it is important to note that they are not mutually exclusives.
You are absolutely free to use many of them and even of different sorts.
Consider this example where we would like to grant access to the checkout page only if the user is authenticated and the basket is not empty ; we would also not want our user to accidentally leave the page if the payment is in progress:
const routes: Route[] = [
{
path: 'checkout',
component: CheckoutComponent,
canActivate: [authenticationGuard, basketNotEmptyGuard],
canDeactivate: [paymentInProgressGuard]
}
];
From a structural point of view, guards also cascade from the parent route to the child route.
It means that guards defined in parents routes will also be evaluated for the child route.
As a result, if you add a guard to ensure that the user is connected on the top of your routes hierarchy, all subsequent routes will be guarded by it.
Inlined Guards
Finally, and thanks to the functional guards, you do not need to create a separate function every time you want to use a guard.
Sometime the logic for a guard is very simple and does not need any additional file or declaration.
As an example, preventing a user to leave the page can be as simple as that:
const routes: Route[] = [
{
path: 'sign-in',
component: SignInComponent,
canDeactivate: [() => !inject(SignInComponent).registrationForm.touched]
}
];
Note that a drawback of this way of writing your guards is that you won't be able to unit test it!
Takeaways
Route guards play a crucial role in controlling and managing access to routes within your application.
In this article, we explored the concept of route guards, their purpose, and how to select the appropriate guard based on specific use cases.
We also learned how to enhance the user experience by leveraging guard hierarchies and implementing the necessary guards. By understanding and utilizing route guards effectively, you can ensure secure and controlled navigation throughout your application.
I hope that you learn something useful there!
Photo by Flash Dantz on Unsplash
Top comments (4)
Maybe i'm wrong but these code line
"const attemptsToAccessItsOwnPage = currentUser.id !== profilePageId;
return attemptsToAccessItsOwnPage;"
mean it return true if user id is not the same than the profile page ? So the guard let the user pass ?
Indeed, thanks for noticing!
De rien :)
Absolutely, this is worth noting!