1. Functional Guards
A guard is a function the router calls before committing to a navigation. Return
true to allow it, false to block it, or a UrlTree
to redirect elsewhere entirely.
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return router.createUrlTree(['/login'], {
queryParams: { redirectTo: state.url },
});
};
{
path: 'projects/:projectId',
component: ProjectDetail,
canActivate: [authGuard],
}
This is exactly the inject() use case from Week 9 — authGuard
is a plain function, no class, no constructor, so inject() is the only way
it can reach AuthService and Router.
CanDeactivate works the same way in reverse — it runs before
leaving a route, commonly used to warn about unsaved changes.
CanMatch is subtly different again: it decides whether a route even
matches a URL in the first place, useful for showing an entirely different
route (rather than just blocking one) based on a condition.
2. Resolvers for Pre-Fetching Route Data
Without a resolver, a component renders immediately with no data, then has to show its own loading state while it fetches. A resolver fetches the data before the navigation completes, so the component's inputs are already populated the instant it renders.
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProjectsService } from './projects.service';
import { Project } from './project.model';
export const projectResolver: ResolveFn<Project> = (route) => {
const projects = inject(ProjectsService);
return projects.getById(route.paramMap.get('projectId')!);
};
{
path: 'projects/:projectId',
component: ProjectDetail,
resolve: { project: projectResolver },
}
The resolved value arrives as a route-data-bound input, same mechanism as
withComponentInputBinding() from last week — add a project =
input.required<Project>() to ProjectDetail and it's populated
automatically, no manual subscription needed.
A resolver delays the navigation itself — the URL doesn't change and no component renders until the resolver's data arrives. That's often the right feel for critical data, but for anything slow or non-essential, letting the component render immediately and show its own loading state (with resource(), Week 16) usually feels faster to a user, even though the data technically arrives at the same time either way.
3. Lazy Loading with loadComponent & loadChildren
By default, every component your routes reference ships in the initial JavaScript
bundle — even ones a user might never visit. loadComponent defers a route's
component (and everything it imports) into its own chunk, downloaded only when that
route is actually navigated to.
export const routes: Routes = [
{ path: '', component: DashboardOverview },
{
path: 'settings',
loadComponent: () =>
import('./settings/settings').then((m) => m.Settings),
},
];
For an entire feature area with its own nested routes, loadChildren lazy-loads
a whole routes array at once:
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes').then((m) => m.adminRoutes),
}
The dynamic import() is what tells the bundler to split this code into a
separate file — not an Angular-specific mechanism, but the standard JavaScript feature
Angular's build tooling is built to recognize and act on.
4. Preloading Strategies
Lazy loading alone means a user waits for the download the moment they click — a preloading strategy downloads lazy chunks after the initial app loads, quietly in the background, so navigation feels instant by the time they actually click.
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
],
};
PreloadAllModules preloads everything, which is often the right default. For
finer control — say, preloading only routes flagged in their data — you can
write a custom PreloadingStrategy class instead, checking each route's
data before deciding whether to preload it.
5. Deferrable Views with @defer
You saw @defer mentioned briefly in Week 4 — here's the full picture.
@defer splits a piece of a template into its own lazy-loaded chunk,
independent of routing entirely. It's for expensive components below the fold, or
rarely-used UI you don't want in the initial bundle.
@defer (on viewport) {
<app-related-products [productId]="product().id" />
} @placeholder {
<div class="skeleton"></div>
} @loading (minimum 200ms) {
<app-spinner />
} @error {
<p>Couldn't load related products.</p>
}
on viewport is one of several triggers — others include on
interaction (a click or keypress), on hover, on timer(5s),
and on idle (the browser's idle callback). @placeholder shows
before the trigger fires; @loading shows while the chunk downloads;
@error covers a failed download. None of this requires a route — a single
component on an otherwise simple page can defer its own heavy children.
6. Route-Level Code-Splitting Strategy
Putting Sections 3–5 together, a reasonable strategy for a real app looks like this:
- Lazy-load every route except the one or two a user hits immediately after login
- Preload everything (or at least high-traffic routes) once the initial app is interactive
- Within a route's own template,
@deferanything expensive that isn't immediately visible — a chart library, a rich-text editor, a modal's contents
You'll measure the real-world effect of this in Week 20 (Performance), using the bundle analysis tools introduced back in Week 2 — for now, the goal is simply having the vocabulary and the instinct for where to reach for each technique.
7. Hands-on Exercise
Guard, resolve, and lazy-load the Week 10 dashboard
Take last week's three-level dashboard and make it production-shaped.
Requirements:
- Add a fake
AuthService(a signal you can toggle) and anauthGuardprotecting every route except a new/loginroute. - Add a
projectResolverthat pre-fetches project data beforeProjectDetailrenders, and bind the resolved value to an input. - Convert every top-level route (dashboard, projects, and the not-found page) to
loadComponent. - Enable
withPreloading(PreloadAllModules)and confirm — via your browser's network tab — that lazy chunks load shortly after the initial app, not only on click. @defer (on viewport)one deliberately "heavy" component inProjectOverview(a fake chart component is fine), with a skeleton@placeholder.
If the guard redirect loop never stops (login redirects back to a protected route, which redirects back to login), check that /login itself isn't accidentally covered by the guard too — it should be one of the few routes explicitly left unprotected.
8. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
What are the three possible return values from a CanActivateFn, and what does each one do?
What are the three possible return values from a CanActivateFn, and what does each one do?
true allows the navigation to proceed; false blocks it (the URL doesn't change and the user stays put); a UrlTree (from router.createUrlTree()) redirects the navigation to a different route entirely, such as a login page.
Q2
What's the practical tradeoff of pre-fetching data with a resolver vs. letting the component load its own data after rendering?
What's the practical tradeoff of pre-fetching data with a resolver vs. letting the component load its own data after rendering?
A resolver means the component never renders in an empty or loading state — but the navigation itself (URL change, any transition animation) waits until the data arrives, which can feel like a delay if the request is slow. Letting the component render immediately and manage its own loading state gets the user to a page faster, at the cost of the component needing to actually build a loading UI.
Q3
What's the difference between what loadComponent and @defer each lazy-load?
What's the difference between what loadComponent and @defer each lazy-load?
loadComponent operates at the routing layer — an entire route's component, loaded only on navigation to that URL. @defer operates inside a single template, independent of routing entirely — a piece of markup within an already-rendered component, loaded based on a trigger like viewport visibility or user interaction, whether or not routing is involved at all.
Q4
If every route is lazy-loaded and preloading is enabled with PreloadAllModules, what's actually different about the user's experience compared to no lazy loading at all?
If every route is lazy-loaded and preloading is enabled with PreloadAllModules, what's actually different about the user's experience compared to no lazy loading at all?
The initial load is faster — only the code for the first route downloads up front, instead of the whole app. Everything else downloads shortly after, in the background, so by the time the user actually navigates elsewhere those chunks are usually already cached. The net effect: faster first paint, with little to no perceived slowdown on subsequent navigation — the best of both approaches, at the cost of some background bandwidth use right after load.