Modern Routing with Standalone Components
Overview
Angular 15+ introduces modern routing with provideRouter, functional guards, and lazy loading via loadComponent.
Basic Setup
main.ts Configuration
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter } from "@angular/router";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes)],
});
Route Configuration
// app.routes.ts
import { Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { AboutComponent } from "./about/about.component";
export const routes: Routes = [
{ path: "", component: HomeComponent },
{ path: "about", component: AboutComponent },
{ path: "**", redirectTo: "" },
];
Lazy Loading with loadComponent
Feature Route Lazy Loading
// app.routes.ts
import { Routes } from "@angular/router";
export const routes: Routes = [
{
path: "dashboard",
loadComponent: () => import("./dashboard/dashboard.component").then((m) => m.DashboardComponent),
},
{
path: "profile",
loadComponent: () => import("./profile/profile.component").then((m) => m.ProfileComponent),
},
];
Lazy Loading Child Routes
// app.routes.ts
export const routes: Routes = [
{
path: "admin",
loadChildren: () => import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
},
];
// admin/admin.routes.ts
import { Routes } from "@angular/router";
export const ADMIN_ROUTES: Routes = [
{
path: "",
loadComponent: () => import("./admin-dashboard.component").then((m) => m.AdminDashboardComponent),
},
{
path: "users",
loadComponent: () => import("./users/user-list.component").then((m) => m.UserListComponent),
},
];
Functional Guards
CanActivate Guard
// guards/auth.guard.ts
import { inject } from "@angular/core";
import { Router } from "@angular/router";
import { AuthService } from "../services/auth.service";
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.createUrlTree(["/login"]);
};
Usage in Routes
import { authGuard } from "./guards/auth.guard";
export const routes: Routes = [
{
path: "dashboard",
loadComponent: () => import("./dashboard/dashboard.component").then((m) => m.DashboardComponent),
canActivate: [authGuard],
},
];
CanDeactivate Guard
// guards/unsaved-changes.guard.ts
import { CanDeactivateFn } from "@angular/router";
Change Detection Deep Dive
Default vs OnPush, signals integration.
Zoneless change detection, markDirty(), explicit triggers.
Advanced DI e<boolean>;
}
export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
return component.canDeactivate ? component.canDeactivate() : true;
};
Functional Resolvers
Basic Resolver
// resolvers/user.resolver.ts
import { inject } from "@angular/core";
import { ResolveFn } from "@angular/router";
import { UserService } from "../services/user.service";
import { User } from "../models/user.model";
export const userResolver: ResolveFn<User> = (route, state) => {
const userService = inject(UserService);
const userId = route.paramMap.get("id")!;
return userService.getUserById(userId);
};
Using Resolver in Routes
import { userResolver } from "./resolvers/user.resolver";
export const routes: Routes = [
{
path: "user/:id",
loadComponent: () => import("./user-detail/user-detail.component").then((m) => m.UserDetailComponent),
resolve: { user: userResolver },
},
];
Accessing Resolved Data
import { Component, inject } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
@Component({
selector: "app-user-detail",
standalone: true,
template: `<h1>{{ user.name }}</h1>`,
})
export class UserDetailComponent {
private route = inject(ActivatedRoute);
user = this.route.snapshot.data["user"];
}
Preloading Strategies
Enable Preloading
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter, PreloadAllModules } from "@angular/router";
import { routes } from "./app/app.routes";
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes, withPreloading(PreloadAllModules))],
});
Custom Preloading Strategy
// strategies/selective-preload.strategy.ts
import { Injectable } from "@angular/core";
import { PreloadingStrategy, Route } from "@angular/router";
import { Observable, of } from "rxjs";
@Injectable({ providedIn: "root" })
export class SelectivePreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data?.["preload"] ? load() : of(null);
}
}
// Usage
export const routes: Routes = [
{
path: "dashboard",
data: { preload: true }, // This will be preloaded
loadComponent: () => import("./dashboard.component").then((m) => m.DashboardComponent),
},
];
Route Parameters and Data
Type-Safe Route Data
// Using route inputs (Angular 16+)
import { Component, Input } from "@angular/core";
@Component({
selector: "app-product",
standalone: true,
template: `<h1>Product {{ id }}</h1>`,
})
export class ProductComponent {
@Input() id!: string; // Automatically populated from route param
}
// Enable in route config
export const routes: Routes = [
{
path: "product/:id",
component: ProductComponent,
// Angular 16+ feature
},
];
Complete Example
// main.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideRouter, withPreloading, PreloadAllModules } from "@angular/router";
import { provideHttpClient } from "@angular/common/http";
import { AppComponent } from "./app/app.component";
import { routes } from "./app/app.routes";
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes, withPreloading(PreloadAllModules)), provideHttpClient()],
}).catch((err) => console.error(err));
// app.routes.ts
import { Routes } from "@angular/router";
import { authGuard } from "./guards/auth.guard";
import { userResolver } from "./resolvers/user.resolver";
export const routes: Routes = [
{
path: "",
loadComponent: () => import("./home/home.component").then((m) => m.HomeComponent),
},
{
path: "dashboard",
loadComponent: () => import("./dashboard/dashboard.component").then((m) => m.DashboardComponent),
canActivate: [authGuard],
},
{
path: "user/:id",
loadComponent: () => import("./user/user-detail.component").then((m) => m.UserDetailComponent),
resolve: { user: userResolver },
},
{
path: "admin",
loadChildren: () => import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES),
canActivate: [authGuard],
},
{ path: "**", redirectTo: "" },
];
Interview Questions
Q: What's the difference between bootstrapModule and bootstrapApplication?
A: bootstrapModule requires an NgModule, while bootstrapApplication works with standalone components and uses provider functions like provideRouter.
Q: How do functional guards differ from class-based guards?
A: Functional guards are simpler functions that use inject() for DI, eliminating boilerplate and making testing easier.
Q: What are the benefits of loadComponent over loadChildren with modules?
A: Direct component loading, smaller bundles, no NgModule overhead, and clearer lazy loading boundaries.