skip to content

In Angular, when is reading ActivatedRoute.snapshot safe, and when must a component subscribe to paramMap instead?

level: middleimportance: must knowfreq 70%

answer

  1. same config, same instance
  2. ngOnInit runs once
  3. snapshot object is replaced
  4. emits only on change

basics

~20 s

The router reuses a component when the next URL matches the same route config, so a snapshot value copied into a field goes stale. Use the snapshot for one-off reads; use paramMap whenever the same component can show new params.

solid answer

~40 s

`ActivatedRoute.snapshot` is an `ActivatedRouteSnapshot`: the route's state at the last completed navigation. The default `RouteReuseStrategy` reuses the component when the next navigation matches the same route config, so `/users/1` to `/users/2` keeps the same `UserProfile` instance, and its constructor and `ngOnInit` do not run again. The router does replace `route.snapshot` on that navigation, so a read at the moment you need the value is current; a value **copied** into a field during construction stays at user 1. `paramMap`, `queryParamMap`, `data` and the other Observables emit their current value on subscribe and again when that part changes. The snapshot is safe when the route cannot reach itself with different params, or when you re-read it at the moment of use. Otherwise subscribe, or use input binding.

code

ts · 23 lines
ts
import {Component, inject} from '@angular/core';
import {toSignal} from '@angular/core/rxjs-interop';
import {ActivatedRoute} from '@angular/router';
import {map} from 'rxjs';

@Component({
  selector: 'app-user-profile',
  template: `<h1>User {{ id() }}</h1>`,
})
export class UserProfile {
  private readonly route = inject(ActivatedRoute);

  // Stale after /users/1 -> /users/2: copied once
  readonly firstId = this.route.snapshot.paramMap.get('id');

  // Follows every navigation, including reused ones
  readonly id = toSignal(this.route.paramMap.pipe(map((p) => p.get('id'))), {requireSync: true});

  // Also current: reads the replaced snapshot at click time
  logId(): void {
    console.log(this.route.snapshot.paramMap.get('id'));
  }
}

go deeper

for a junior

Remember that the snapshot does not update and paramMap does, and that the same component can be reused for a different id.

for a middle

Explain the default reuse rule, that route.snapshot is replaced while copied values go stale, and which Observable emits for which kind of change.

for a senior

Default to reactive reads on pages that can link to themselves, and subscribe to the narrowest Observable so query-only changes do not refetch everything.

for a principal

Set conventions that survive later features, since a related-items link added years on can silently break every snapshot-based detail page.

## Two shapes of the same state An `ActivatedRoute` exposes each piece of route state twice: | Piece | Observable | Snapshot | |---|---|---| | path and matrix params | `paramMap`, `params` | `snapshot.paramMap`, `snapshot.params` | | query params | `queryParamMap`, `queryParams` | `snapshot.queryParamMap`, `snapshot.queryParams` | | fragment | `fragment` | `snapshot.fragment` | | static and resolved data | `data` | `snapshot.data` | | matched segments | `url` | `snapshot.url` | The **snapshot** is a plain `ActivatedRouteSnapshot` describing the route after the last completed navigation. The **Observables** emit the current value when you subscribe and then push new values as navigations change them. ## Why the difference matters: component reuse On every navigation the router compares the new route tree with the current one. The default `RouteReuseStrategy.shouldReuseRoute()` returns `true` when the future and current route share the **same route config object**. For `users/:id`, going from `/users/1` to `/users/2` matches the same config, so: 1. the `UserProfile` component instance is **kept**; 2. its constructor and `ngOnInit` **do not run again**; 3. the router updates the existing `ActivatedRoute`: it replaces `route.snapshot` with the new snapshot and pushes new values into the Observables whose content changed. Code that read the id once, in the constructor or `ngOnInit`, keeps showing user 1. ## The snapshot object is replaced, not frozen The subtle part: `this.route.snapshot` is re-pointed to the new snapshot on each navigation. Two patterns behave differently: - `readonly id = this.route.snapshot.paramMap.get('id')` copies the value once, so it is **stale** after reuse; - `save() { const id = this.route.snapshot.paramMap.get('id'); ... }` reads at click time, so it is **current**. The snapshot is not broken; copying it is. It cannot notify anything, though, so templates and derived state that must update still need the Observable or an input. ## When the Observables emit For a reused route the router compares old and new values and only emits what changed: - `paramMap` / `params` emit when the route's params differ by shallow comparison, so `/users/2` to `/users/2?tab=posts` does **not** re-emit `paramMap`; - `queryParamMap` emits when the query string changes, and the query is shared by every route in the tree; - `fragment` emits when the fragment changes; - `data` emits when static or resolved data differs. This is why subscribing to the precise Observable you need avoids refetching the user when only a tab query param changes. ## When the snapshot is safe 1. The component can never be reached from itself with different params: no Next or Previous link, no search box re-navigating the same route. 2. You read `this.route.snapshot` at the moment of use, in an event handler. 3. The value is used exactly once during setup and never needs to follow the URL. The first condition is fragile: a feature added later, such as a related-users list linking to `/users/:id`, breaks it without touching the component. Many teams therefore default to the reactive form on detail pages. ## Queries and data are shared differently The query string belongs to the whole URL, so every `ActivatedRoute` in the tree reports the same `queryParamMap`, and a child can react to `?tab=posts` without reaching up. Path params belong to the route that declared them, plus whatever the inheritance strategy passes down. `data` combines the route's static `data` with values from its resolvers; it re-emits on a reused route when a resolver produces a different value or the static data differs. ## Reactive options in current Angular - Subscribe to `this.route.paramMap` and derive the id in the pipeline. - Convert it to a signal with `toSignal()` from `@angular/core/rxjs-interop`. - Enable `withComponentInputBinding()` and declare an input named `id`; the router sets it on every navigation, including reused ones, and a `computed()` follows it. All three survive reuse. The mistake they share a fix for is reading route state **once** in a component the router is allowed to keep.

  • Does route.paramMap emit again when only the query string changes?
    No. For a reused route the router compares old and new params and emits paramMap only when they differ. A query-only change emits on queryParamMap instead, which lets a component refetch only when the path parameter it depends on actually changed.
  • Why does ngOnInit not run when navigating from /users/1 to /users/2?
    The default RouteReuseStrategy reuses the component because both URLs match the same route config. The instance is kept and only its ActivatedRoute is updated, so lifecycle hooks that run once per instance do not run again.

The snapshot is a photo of the departures board: accurate when taken, and a new photo is pinned up after every change. Copying the gate number into your notebook freezes it; the Observable is standing in front of the live board.

saying these in an interview costs you the question

  • Believing the router always destroys and recreates the component on navigation
  • Thinking route.snapshot itself never changes after the component is created
  • Expecting paramMap to emit on every navigation, even query-only ones
  • Moving the snapshot read from the constructor to ngOnInit as a fix
  • Claiming snapshots are unsafe to read inside event handlers