Route Parameters in Angular
Route parameters let you embed dynamic values directly in the URL — /users/42, /products/laptop-pro, /blog/2024/angular-routing. The component reads those values from the URL to load the right data.
Angular supports three types of URL-based data passing:
- Route parameters — part of the path:
/users/:id - Query parameters — after the
?:/search?q=angular&page=2 - Fragment — after the
#:/docs/routing#advanced
Defining Route Parameters
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: 'users', component: UserListComponent },
{ path: 'users/:id', component: UserDetailComponent }, // :id is a parameter
// Multiple parameters
{ path: 'blog/:year/:month/:slug', component: BlogPostComponent },
// Optional segment (use separate routes or query params for optional data)
{ path: 'products/:category', component: ProductListComponent },
{ path: 'products/:category/:id', component: ProductDetailComponent },
];Reading Parameters: snapshot vs Observable
There are two ways to read route parameters:
snapshot— reads the current value once; fine when the component is always destroyed and recreated on navigationparamMapobservable — emits whenever the parameter changes; necessary when the same component instance is reused (e.g. navigating from/users/1to/users/2)
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
@Component({
standalone: true,
template: `
<div *ngIf="user">
<h1>{{ user.name }}</h1>
<p>{{ user.email }}</p>
</div>
`,
})
export class UserDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private userService = inject(UserService);
user: User | null = null;
ngOnInit() {
// Option 1: snapshot — reads once
const id = this.route.snapshot.paramMap.get('id');
if (id) this.loadUser(+id);
// Option 2: observable — reacts to changes
this.route.paramMap.subscribe(params => {
const id = params.get('id');
if (id) this.loadUser(+id);
});
}
private loadUser(id: number) {
this.userService.getUser(id).subscribe(user => (this.user = user));
}
}+ prefix converts the string parameter to a number: +id is equivalent to Number(id). Route parameters are always strings in Angular.Input Binding for Route Parameters (Angular 16+)
Angular 16 introduced a cleaner way to receive route parameters: bind them directly to component @Input properties using withComponentInputBinding().
// main.ts — enable input binding
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withComponentInputBinding } from '@angular/router';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withComponentInputBinding()),
],
});// user-detail.component.ts — receive :id as an @Input
import { Component, Input, OnChanges, inject } from '@angular/core';
import { UserService } from './user.service';
@Component({
standalone: true,
template: `<h1>User {{ id }}</h1>`,
})
export class UserDetailComponent implements OnChanges {
@Input() id!: string; // automatically populated from :id param
private userService = inject(UserService);
ngOnChanges() {
// Called every time id changes (same component, different param)
this.userService.getUser(+this.id).subscribe(/* ... */);
}
}withComponentInputBinding(), route params, query params, and resolved route data are all automatically bound to matching @Input properties. This is the cleanest approach for Angular 16+ projects.Query Parameters
Query parameters are ideal for optional, filter-like data that doesn't change which component is shown — search terms, pagination, sort order.
// Navigate with query params
import { Router } from '@angular/router';
const router = inject(Router);
router.navigate(['/products'], {
queryParams: { category: 'electronics', page: 2, sort: 'price-asc' },
});
// Preserve existing query params when navigating
router.navigate(['/products'], {
queryParams: { page: 3 },
queryParamsHandling: 'merge', // keeps other existing params
// queryParamsHandling: 'preserve' — keeps ALL existing params unchanged
});<!-- routerLink with query params -->
<a
[routerLink]="['/products']"
[queryParams]="{ category: 'books', page: 1 }"
>Books</a>
<!-- Link that adds to existing query params -->
<a
[routerLink]="['/products']"
[queryParams]="{ page: nextPage }"
queryParamsHandling="merge"
>Next Page</a>// Reading query params
import { ActivatedRoute } from '@angular/router';
const route = inject(ActivatedRoute);
// Snapshot (once)
const category = route.snapshot.queryParamMap.get('category');
const page = +(route.snapshot.queryParamMap.get('page') ?? '1');
// Observable (reacts to changes)
route.queryParamMap.subscribe(params => {
const category = params.get('category') ?? '';
const page = +(params.get('page') ?? '1');
// load data...
});
// As a plain object
route.queryParams.subscribe(params => {
console.log(params['category'], params['page']);
});Fragment (#anchor)
// Navigate with fragment
router.navigate(['/docs/routing'], { fragment: 'advanced' });
// URL becomes: /docs/routing#advanced<a [routerLink]="['/docs/routing']" fragment="advanced">Advanced Routing</a>
// Read the fragment
route.fragment.subscribe(fragment => {
if (fragment) {
document.querySelector(`#${fragment}`)?.scrollIntoView();
}
});Passing Static Data via Route Config
Use data in the route config to pass static metadata — page titles, breadcrumbs, permissions — to a component without URL-visible values.
// app.routes.ts
const routes: Routes = [
{
path: 'admin/users',
component: AdminUsersComponent,
data: {
title: 'User Management',
breadcrumb: ['Admin', 'Users'],
requiredRole: 'ADMIN',
},
},
];
// Reading in the component
const title = route.snapshot.data['title'];
const role = route.snapshot.data['requiredRole'];Comparison: Ways to Pass Data via URL
Method | Example URL | Best for |
|---|---|---|
Route param | /users/42 | Required, identity-defining values (ID, slug) |
Query param | /search?q=angular&page=2 | Optional filters, pagination, sorting |
Fragment | /docs#section | In-page anchors, scroll targets |
Route data | (not in URL) | Static metadata like title or required role |
State (history) | (not in URL) | Transient data like flash messages |
Navigation State (Transient Data)
Passing sensitive or large data through the URL is undesirable. Use router state to pass data invisibly through navigation history.
// Sender component
router.navigate(['/checkout/success'], {
state: { orderId: 'ORD-123', total: 99.99 },
});
// Receiver component
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({ standalone: true, template: `...` })
export class CheckoutSuccessComponent {
private router = inject(Router);
// State is available via the router's current navigation
orderId: string = this.router.getCurrentNavigation()?.extras.state?.['orderId'] ?? '';
// or via window.history.state (after the navigation completes)
}window.history.state and is lost on page refresh. Do not rely on it for data that must survive a full reload — store that data in a service or session storage instead.The ActivatedRoute Snapshot vs Observable Summary
Approach | When to use |
|---|---|
snapshot.paramMap.get() | Component is always destroyed on navigation (most cases) |
paramMap observable | Same component instance reused when only the param changes |
@Input() binding (v16+) | Cleanest modern approach — Angular wires everything automatically |
Summary: Use :paramName in route paths for required URL segments and read them via ActivatedRoute. Use query parameters for optional filter-like data. Angular 16+ withComponentInputBinding() is the cleanest approach — just declare an @Input with the same name as the parameter and Angular populates it automatically.