Route Parameters and Query Params
Real applications rarely have a fixed, finite set of pages — a product catalog needs one route that works for thousands of products, and a search page needs to reflect filters in the URL so results are shareable and bookmarkable. Angular's router supports this through two complementary mechanisms: route (path) parameters, which are required segments embedded directly in the URL path and identified with a colon prefix like :id, and query parameters, which are optional key-value pairs appended after a ? and are independent of route matching. Both are readable from the injected ActivatedRoute service, either as static snapshots or as reactive observables that update when the same component instance is reused for a new parameter value.
Cricket analogy: Route params versus query params are like a stadium gate number that's required to find your seat block (:id in the path) versus optional add-ons like a parking pass or programme (?parking=true) that don't affect which gate you use.
Defining and Reading Route Parameters
A route parameter is declared in the path string itself, e.g. { path: 'products/:id', component: ProductDetailComponent }. When the URL is /products/42, Angular matches this route and makes id: '42' available. Because Angular reuses component instances by default when navigating between two URLs that match the same route (e.g. /products/42 to /products/43), reading the parameter only once in ngOnInit via a snapshot will miss subsequent changes — the component won't be destroyed and recreated. For that reason, the recommended approach is to subscribe to ActivatedRoute.paramMap, an observable that emits a new ParamMap every time the parameters change.
Cricket analogy: Angular reusing the component instance between /products/42 and /products/43 is like a commentary booth staying staffed by the same commentator across consecutive overs of the same match rather than being replaced each over, so they must actively watch for the new ball rather than assuming nothing changed.
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map, switchMap } from 'rxjs/operators';
import { ProductService } from './product.service';
@Component({
selector: 'app-product-detail',
standalone: true,
template: `
@if (product(); as p) {
<h2>{{ p.name }}</h2>
<p>Price: {{ p.price | currency }}</p>
} @else {
<p>Loading product...</p>
}
`,
})
export class ProductDetailComponent {
private route = inject(ActivatedRoute);
private productService = inject(ProductService);
// paramMap$ -> id -> product, converted to a signal for the template
product = toSignal(
this.route.paramMap.pipe(
map((params) => params.get('id')!),
switchMap((id) => this.productService.getById(id))
)
);
}Query Parameters
Query parameters are not declared in the route path at all — any route can receive any query params. They're ideal for optional, non-hierarchical state such as pagination (?page=2), sorting (?sort=price), or search filters (?category=books&inStock=true). You read them via ActivatedRoute.queryParamMap, which behaves like paramMap but for the query string, and you set them either through routerLink's [queryParams] input or by passing a queryParams option to router.navigate().
Cricket analogy: Query params for sorting and filtering are like a scorecard app letting you append ?sort=runs&format=t20 to any match URL without changing which match page you're on, purely adjusting how the stats are displayed.
// Template: link that adds/updates query params without changing the path
// <a [routerLink]="['/products']" [queryParams]="{ category: 'books', page: 2 }">Books, page 2</a>
import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
@Component({ selector: 'app-filters', standalone: true, template: `...` })
export class FiltersComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
applyFilter(category: string): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { category },
queryParamsHandling: 'merge', // keep existing params like 'page'
});
}
}queryParamsHandling: 'merge' is a subtle but important option — without it, navigating with new queryParams replaces the entire query string, silently discarding unrelated params like pagination or sort order that the user had already set.
Subscribing to paramMap or queryParamMap manually with .subscribe() creates an Observable subscription that must be unsubscribed in ngOnDestroy to avoid memory leaks — unless you use the async pipe in the template or toSignal(), both of which handle cleanup automatically.
- Route parameters (
:id) are required, path-embedded values declared in the route'spathstring. - Query parameters are optional key-value pairs after
?, independent of route matching, ideal for filters/pagination. - Use
ActivatedRoute.paramMap/queryParamMapobservables rather than one-time snapshots, since Angular reuses component instances. toSignal()or theasyncpipe convert these observables into template-friendly values with automatic unsubscription.- Use
queryParamsHandling: 'merge'to update one query param without wiping out the others. - Static snapshots (
route.snapshot.paramMap) only reflect the params at initial load, missing later in-place navigations.
Practice what you learned
1. How is a required route parameter declared in a route path?
2. Why should you subscribe to `ActivatedRoute.paramMap` instead of reading `route.snapshot.paramMap` once in `ngOnInit`?
3. Which router option preserves existing query parameters when navigating with new ones?
4. What kind of URL data are query parameters best suited for?
5. What RxJS operator is typically used to turn an emitted route parameter into a new HTTP request observable?
Was this page helpful?
You May Also Like
Angular Router Basics
Learn how Angular's built-in router maps URL paths to components, enabling single-page applications to feel like multi-page sites without full page reloads.
Route Guards
Learn how functional route guards control navigation access — protecting routes behind authentication, confirming unsaved changes, and pre-fetching data before activation.
RxJS Operators for HTTP (map, switchMap, catchError)
Master the core RxJS operators — map, switchMap, catchError, and their flattening cousins — used to transform, chain, and recover from HTTP requests.
HttpClient and Fetching Data
Learn how Angular's HttpClient issues typed, Observable-based HTTP requests, and how to provide it in a standalone application.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics