Routing
Routes, params, guards and lazy loading. Q29 contains the bug everyone hits once.
How does routing work?
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'orders', component: OrderListComponent },
{ path: 'orders/:id', component: OrderDetailComponent },
{ path: 'login', component: LoginComponent },
{ path: '**', component: NotFoundComponent } // wildcard LAST
];
@NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] })
export class AppRoutingModule { }
<nav>
<a routerLink="/orders" routerLinkActive="active">Orders</a>
</nav>
<router-outlet></router-outlet> <!-- the matched component renders here -->
forRoot | Once, in the app module |
forChild | In every feature module |
routerLink | Not href — href reloads the whole app |
** | Wildcard for 404. Must be the last route — matching is first-wins |
Navigating in code: this.router.navigate(['/orders', id]).
Reading route parameters
You're on /orders/1 and navigate to /orders/2. Angular
reuses the same component instance — it doesn't destroy and recreate it. So
ngOnInit never runs again, and the screen still shows order 1.
// ❌ only works the first time
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
this.load(id);
}
// ✅ re-runs whenever the id changes
ngOnInit() {
this.route.paramMap
.pipe(takeUntil(this.destroy$))
.subscribe(p => this.load(p.get('id')!));
}
// ✅ even better — switchMap cancels the previous request
orders$ = this.route.paramMap.pipe(
switchMap(p => this.api.get(Number(p.get('id'))))
);
paramMap | Path params — /orders/:id |
queryParamMap | Query string — ?page=2&sort=date |
snapshot | A one-off read. Only safe if the component is always recreated |
That last version pairs nicely with the async pipe — no manual subscribe, nothing to
unsubscribe, and the in-flight request is cancelled when the user clicks the next order.
{
path: 'orders/:id',
component: OrderDetailComponent, // has its own <router-outlet>
children: [
{ path: '', redirectTo: 'summary', pathMatch: 'full' },
{ path: 'summary', component: SummaryComponent },
{ path: 'lines', component: LinesComponent },
{ path: 'history', component: HistoryComponent }
]
}
The parent stays mounted — its header, tabs and loaded order stay put — while only the inner view changes. That's what you want for a tabbed detail screen.
pathMatch: 'full' on the empty-path redirect matters: without it, the empty path
matches every URL prefix and you get a redirect loop.
Route guards
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(): boolean | UrlTree {
return this.auth.isLoggedIn()
? true
: this.router.createUrlTree(['/login']); // redirect, don't just block
}
}
{ path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }
| Guard | Answers |
|---|---|
CanActivate | Can you enter this route? |
CanActivateChild | Can you enter its children? |
CanDeactivate | Can you leave? — "you have unsaved changes" |
Resolve | Fetch data before the route activates |
Returning a UrlTree redirects. Returning false just blocks, which
leaves the user staring at the page they were already on with no explanation.
Angular 15+: guards are plain functions —
canActivate: [() => inject(AuthService).isLoggedIn()]. Less ceremony, same job.
It only stops navigation in the browser. Anyone can call the API directly. The server must check authorisation too — the guard is there for user experience.
Lazy loading
A feature is only downloaded when the user first goes there, so the initial bundle stays small.
// module-based
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
}
// standalone component (Angular 14+)
{
path: 'reports',
loadComponent: () => import('./reports/reports.component').then(m => m.ReportsComponent)
}
Check the numbers in the build output — ng build prints the size of every chunk.
The admin area is usually the first thing worth splitting out: most users never open it.
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) keeps the
small initial bundle but quietly downloads the rest after startup — so the first click into
/admin is instant. Best of both for most apps.