In an Angular product-detail page, when does ActivatedRoute.paramMap emit, and why does that matter when navigating from /products/1 to /products/2?
answer
- BehaviorSubject behind params
- current value on subscribe
- same route config, same instance
- shallow-equal params do not emit
- the stream never completes
basics
~20 sparamMap emits the current params synchronously on subscribe and emits again whenever the params change while the component instance is reused; it never completes. Moving from /products/1 to /products/2 reuses the component, so only paramMap, not a one-time snapshot read, sees the new id.
solid answer
~40 s`ActivatedRoute.params` is backed by a `BehaviorSubject`, and `paramMap` maps it to a `ParamMap`. So subscribing hands you the current params **immediately and synchronously**, and each later change produces another emission. The router only pushes new params when they differ from the previous ones by shallow equality. The default route reuse strategy keeps the same component instance when the next route uses the same route config, so going from `/products/1` to `/products/2` does not recreate `ProductDetail` or re-run `ngOnInit`. Code that read `route.snapshot.paramMap.get('id')` once keeps showing product 1; code that subscribes to `paramMap` gets `'2'` and can load the new product. The stream never completes, so its subscription ends only by teardown.
code
ts · 25 linesimport { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-product-detail',
template: `
<p>Snapshot id: {{ snapshotId }}</p>
<p>Live id: {{ liveId() }}</p>
`,
})
export class ProductDetail {
private readonly route = inject(ActivatedRoute);
// Read once: stays '1' when the router reuses this component for /products/2.
readonly snapshotId = this.route.snapshot.paramMap.get('id');
// Updated on every param change.
readonly liveId = signal<string | null>(null);
constructor() {
// Fires synchronously with the current id, then on each change.
// The subscription never completes; teardown is required in real code.
this.route.paramMap.subscribe((params) => this.liveId.set(params.get('id')));
}
}go deeper
Recall that paramMap is an Observable that gives the current id immediately and again when it changes, unlike a one-time snapshot read.
Explain the BehaviorSubject backing, shallow-equality emission and why route reuse means ngOnInit does not run for a new id.
Diagnose stale detail pages caused by snapshot reads and treat the never-completing paramMap subscription as long-lived.
Decide a team default for reading route state, whether streams, input binding or snapshots, and when reuse makes each one safe.
## The scenario A shop has the route `{ path: 'products/:id', component: ProductDetail }`. The detail page shows "Related products" links, and clicking one navigates from `/products/1` to `/products/2`. The URL changes, but the page keeps showing product 1. The cause is how `ActivatedRoute` exposes the id, and how the router reuses components. This answer assumes Angular 22.2. ## What backs paramMap The router creates an `ActivatedRoute` for each routed component. Its Observables are built on `BehaviorSubject`s: - `params`, `queryParams`, `fragment`, `data` and `url` are each a `BehaviorSubject` the router pushes into. - `paramMap` and `queryParamMap` are `params` and `queryParams` piped through `map()` into a `ParamMap`, which adds `get()`, `getAll()` and `has()`. Three behaviours follow: 1. **Synchronous current value.** A `BehaviorSubject` gives each new subscriber its current value at once, so `paramMap.subscribe()` fires during the `subscribe()` call itself. 2. **Emission on change only.** When a navigation updates an existing route, the router compares old and new params with **shallow equality** and calls `next()` only if they differ. The same holds for `queryParams`, `data` and `url`. 3. **No completion.** The router never completes these subjects. A subscription stays open until something unsubscribes it. ## Why the component is reused The default `RouteReuseStrategy` reuses the current route's component when the next route has the **same route config**. `/products/1` and `/products/2` both match `products/:id`, so Angular keeps the same `ProductDetail` instance and the same `ActivatedRoute`, and just pushes new params into it: | Reading the id | After `/products/1` → `/products/2` | | --- | --- | | `route.snapshot.paramMap.get('id')` read once in `ngOnInit` | Still `'1'` in your field; `ngOnInit` does not run again | | `route.paramMap.subscribe(...)` | Emits a new `ParamMap` with `'2'` | | `route.snapshot` read again later | Reflects the new snapshot, but only if something reads it again | The snapshot object itself is replaced on the route; the bug is reading it only once. ## Loading the product from the stream ```ts constructor() { this.route.paramMap .pipe( map((params) => params.get('id')!), switchMap((id) => this.http.get<Product>(`/api/products/${id}`)), takeUntilDestroyed(), ) .subscribe((product) => this.product.set(product)); } ``` Each emission maps to an id and starts a request; `switchMap` drops the response for an id the user has already left, and `takeUntilDestroyed()` ends the never-completing subscription with the component. The operators and the teardown helper are covered in their own topics; what matters here is that the stream, not the snapshot, carries every id the reused component will show. ## When the snapshot is fine - If a route can never be reused for a different param — for example because every link to it leaves for another route first — reading the snapshot once is simpler. - If you bind route params to component inputs with the router's input binding feature, the router sets the inputs on each change for you. ## Version note Since Angular 22 the router's default `paramsInheritanceStrategy` is `'always'`, so a child route's `paramMap` also contains its parents' params, such as `:id` in a `products/:id/reviews` child. Earlier versions defaulted to `'emptyOnly'`. ## Summary - `paramMap` replays the current params on subscribe and emits again on real changes. - Reused components never re-run `ngOnInit` for a param change. - The stream does not complete; treat its subscription as long-lived.
- Why does navigating from /products/1 to /products/2 not run ngOnInit again?The default route reuse strategy keeps the current component when the next route has the same route config. Both URLs match `products/:id`, so Angular keeps the instance and pushes the new params into its existing `ActivatedRoute`; no new component means no new `ngOnInit`.
- Does paramMap emit if only the query string changes on the same product?No. Query parameters live in `queryParams` and `queryParamMap`, which have their own subject. `paramMap` emits only when the route's own params change by shallow equality, so a changed `?tab=reviews` updates `queryParamMap` instead.
saying these in an interview costs you the question
- paramMap only emits after the first navigation, not on subscribe.
- Navigating to a new id always creates a new component instance.
- route.snapshot is a live object that updates fields you copied from it.
- The router completes paramMap when the navigation finishes.
- paramMap emits whenever the query string changes.