Routing
`provideRouter` instead of `RouterModule.forRoot`, route titles as data, params bound straight into signal inputs, and guards that are just functions.
The router is a provider now
2022 — module config, and a class guard
2026 — providers, and a function
The functional guard has no class to register, so nothing has to appear in a provider array — and because it returns a UrlTree instead of navigating imperatively, the redirect is expressed as a value the router can act on.
Route titles
The title used to be set in a component's ngOnInit with Title.setTitle, or left to whatever the page happened to be. It is a route property now, and it can be a function of the route:
export const routes: Routes = [
{ path: '', component: Home, title: 'Angular Primer' },
{ path: 'topics/:slug', loadComponent: () => import('./pages/topic/topic').then((m) => m.Topic),
title: (route) => `${route.paramMap.get('slug')} — Angular Primer` },
{ path: '**', component: NotFound, title: 'Not found' },
];Two things follow from putting the title here. The router sets it on navigation without any component doing work, and a prerendered or server-rendered page ships the right <title> in the HTML — which was the part that never worked when the title was set in a lifecycle hook.
Params as signal inputs
withComponentInputBinding() binds route data — params, queryParams, data, resolve — into component inputs, so a page never has to inject ActivatedRoute and subscribe:
2022 — read the param stream, remember to unsubscribe
2026 — an input, bound by the router
The router writes into the input on every navigation, so "the parameter changed" is just "the signal changed" — and anything derived from it, like the resource above, reloads on its own.
Route params as inputs
The routed component takes the parameter as a required input, so it cannot exist without one. Press a link: the component is created with the value already in place.
the address bar would read /orders/…
No route parameter, so the detail component is not created at all — `input.required()` means it cannot exist without one.
With withComponentInputBinding() the router hands :id to the routed component as an input, so the component is created with the value already in place — no ActivatedRoute, no subscription, nothing to unsubscribe. A page cannot mount a second router inside itself, so this demo drives that same input from a signal; the route configuration and the withComponentInputBinding() registration are in the code panel.
Lazy routes without modules
loadComponent for a single component, loadChildren for a slice of routes:
const routes: Routes = [
{ path: 'cheatsheet', loadComponent: () => import('./pages/cheatsheet/cheatsheet').then((m) => m.Cheatsheet) },
{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then((m) => m.ADMIN_ROUTES),
canActivate: [authGuard] },
];A guard on a lazy route still runs before the chunk is fetched, which is the behaviour you want: an unauthorised user should not download the admin bundle to be told no.
View transitions
withViewTransitions() wires the browser's View Transitions API into navigation, so a route change can animate between the two pages. Two practical notes: it degrades to an instant swap where the API is missing, and it should respect prefers-reduced-motion — this site disables the transition for users who ask for that.
provideRouter(routes, withViewTransitions({ skipInitialTransition: true }))A guard is just a function
canMatch reads the session through inject() and answers per navigation. Try it signed out, sign in, try again — the log is the real guard running.
session: signed out · the guard says: none
Try it signed out, then sign in and try again: the guard runs on every navigation, not once at startup, and it reads the same SessionState the component does. The demo calls the real guard function inside the component's injector — the router would call it for you.
Resolvers, still functions
A resolver that blocks navigation until data exists has the same shape as a guard:
export const pageResolver: ResolveFn<Page> = (route) => {
const api = inject(ContentApi);
return api.page(route.paramMap.get('slug')!);
};Keep the bar high for using one. A resolver that awaits a slow network call leaves the user staring at the previous page; with signals, rendering the shell and letting a resource() show a skeleton is usually the better experience. Resolvers are for data the page cannot meaningfully render without.
What to take away
Routing became configuration instead of mechanism: providers for setup, functions for the seams where you need logic, and route data flowing into signals so a page reacts to a param the same way it reacts to anything else. Next: HTTP & async data, which is where those signals usually get their values from.