| |

Angular 23 🅰️ Route Parameters and Query Params

A route that always renders the same thing is limited. Real apps pass data through the URL — /users/42 shows user 42, /search?q=hello runs a search, /products?page=2&sort=price paginates and sorts. Angular’s router exposes that data through route parameters and query parameters. Route params are part of the path itself; query params come after the ?. Both are available through ActivatedRoute — as a snapshot for one-time reads or as an observable when the value can change without the component being recreated. This chapter covers reading both, the difference between snapshot and observable, and the patterns that make them reliable.

Key point: The URL has two parts you can read data from: the path (/users/42) and the query string (?q=hello&page=2). Route params are captured from the path pattern — :id in the config matches a segment. Query params are key-value pairs after the ?. Both are read from ActivatedRoute. The critical decision is snapshot or observable: use snapshot for one-time reads, use the observable when the same component may be reused with different params (like navigating from /users/1 to /users/2).


What route parameters are

A route parameter is a placeholder in the route path that captures a segment of the URL.

export const routes: Routes = [
  { path: 'users/:id', component: UserDetailComponent }
];

:id is the parameter. Navigating to /users/42 matches this route, and the router captures id = '42'.

Reading the parameter:

import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({ /* ... */ })
export class UserDetailComponent {
  private route = inject(ActivatedRoute);

  ngOnInit(): void {
    const id = this.route.snapshot.paramMap.get('id');
    console.log(id);   // '42' (string) or null
  }
}

route.snapshot.paramMap.get('id') returns the string value or null.

Parameters are always strings: The URL is text. paramMap.get('id') returns string | null. Convert to numbers if needed.

const id = Number(this.route.snapshot.paramMap.get('id'));

Multiple parameters:

{ path: 'users/:userId/posts/:postId', component: PostComponent }

Visiting /users/1/posts/42 captures userId = '1' and postId = '42'.

Why route params matter: They make routes parameterized. /users/42 and /users/43 render the same component with different data. That’s how a list-detail app works — one component, many URLs.

The shape: paramMap is a ParamMap — a read-only map of parameter names to values. get returns string | null; getAll returns string[] (for repeated params).

Why parameters are strings: URLs are strings. The browser doesn’t parse them into typed values. Angular passes them through as-is and leaves the conversion to you. That’s consistent with how query strings work everywhere.


Snapshot vs observable

ActivatedRoute gives the same data two ways: a snapshot (value at activation) and observables (which emit on change).

Snapshot:

const id = this.route.snapshot.paramMap.get('id');

Returns the value at the moment the component was created. Doesn’t update.

Observable:

this.route.paramMap.subscribe(params => {
  const id = params.get('id');
});

Emits the current value and every subsequent change.

When the component is reused: Navigating from /users/1 to /users/2 usually reuses the same UserDetailComponent instance. Angular sees the same route, same component — no reason to destroy and recreate.

// URL: /users/1 → component created
// Snapshot: id = '1'
// Observable: emits '1'

// URL changes: /users/2 → component reused
// Snapshot: id is still '1' ← stale!
// Observable: emits '2' ← correct

The rule:

SituationUse
Params never change for this componentSnapshot
Params may change (same route reused)Observable
UnsureObservable

Why the difference: Snapshot captures the value once. If the URL changes without recreating the component, the snapshot is outdated. The observable stays in sync.

The safe default: Use the observable. It works in both cases and never goes stale.

Unsubscribing: Route observables complete when the component is destroyed — no manual unsubscribe needed. That’s one of the router’s conveniences.

Why Angular reuses the component: Recreating a component is expensive. If the route config matches and the component class is the same, Angular reuses the instance. It only recreates when the path genuinely changes to a different route.

Why both APIs exist: Snapshot is convenient for one-time reads. The observable is correct for the reuse case. Angular provides both because they serve different needs. When in doubt, subscribe — the observable handles both.


Reading route params

The paramMap provides both get and getAll.

// Snapshot
const id = this.route.snapshot.paramMap.get('id');

// Observable
this.route.paramMap.subscribe(params => {
  const id = params.get('id');
});

get vs getAll:

MethodReturns
get(key)string | null — first value
getAll(key)string[] — all values
has(key)boolean

Repeated params: A route like :ids can match multiple segments if configured with a repeated pattern. Rare, but getAll handles it.

Multiple parameters:

this.route.paramMap.subscribe(params => {
  const userId = params.get('userId');
  const postId = params.get('postId');
  console.log(userId, postId);
});

Each parameter is keyed by the name from the route config.

Pattern with signals: Combine with toSignal to get a signal from the observable.

import { toSignal } from '@angular/core/rxjs-interop';

export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  id = toSignal(this.route.paramMap.pipe(map(p => p.get('id'))));
}

id is a signal that updates on navigation.

Pattern with switchMap: Load data whenever the param changes.

this.route.paramMap.pipe(
  map(params => params.get('id')),
  switchMap(id => this.userService.getUser(Number(id)))
).subscribe(user => {
  this.user = user;
});

The outer observable reacts to param changes; switchMap cancels the previous request and starts a new one.

Pattern with takeUntilDestroyed: Subscribe and clean up automatically.

this.route.paramMap.pipe(
  takeUntilDestroyed()
).subscribe(params => {
  const id = params.get('id');
  this.loadUser(Number(id));
});

Route observables complete on destroy, but takeUntilDestroyed covers cases where the subscription outlives the component for other reasons.

Why paramMap and not params: paramMap has a typed API (get, getAll, has). params is a plain object — also works but less ergonomic. Use paramMap for new code.

Why paramMap was introduced: Earlier Angular had params as an object and paramsArray as an array. paramMap unified them — get returns the first, getAll returns all. It’s the modern, consistent API.


What query parameters are

Query parameters come after the ? in a URL — key-value pairs that don’t change which component renders.

/users?role=admin&page=2

role=admin and page=2 are query params. The route still matches /users.

Reading query params:

this.route.queryParamMap.subscribe(params => {
  const role = params.get('role');
  const page = params.get('page');
  console.log(role, page);
});

// Or snapshot
const role = this.route.snapshot.queryParamMap.get('role');

queryParamMap works like paramMapget, getAll, has.

Route params vs query params:

AspectRoute ParamsQuery Params
PositionIn the path (/users/:id)After ?
RequiredYes (part of route)Optional
Affects routingYesNo
Use forIdentityFilters, options
Example/users/42/users?page=2

When to use which:

  • Route params — the resource identity. /users/42 — user 42.
  • Query params — optional settings. /users?page=2&sort=name — same list, different view.

Why the distinction: Route params are part of the route — a different value is a different “page.” Query params are modifiers — they don’t change the route, they change how the same route behaves.

Why query params matter: Filters, pagination, sorting, search queries — anything optional and non-identifying. They don’t break the route match, and they can be omitted.

Why use query params for filters: Because filters are optional. /users is valid without a filter. /users?role=admin adds a filter. If the filter were a route param, you’d need two routes — one with and one without. Query params keep the route single.


Reading query params

Same API as route params, but through queryParamMap.

// Snapshot
const page = this.route.snapshot.queryParamMap.get('page');

// Observable
this.route.queryParamMap.subscribe(params => {
  const page = params.get('page');
  const sort = params.get('sort');
});

Defaults: Query params are optional. If missing, get returns null.

const page = this.route.snapshot.queryParamMap.get('page') ?? '1';
const sort = this.route.snapshot.queryParamMap.get('sort') ?? 'name';

Parsing types:

const page = Number(this.route.snapshot.queryParamMap.get('page') ?? '1');

Query param values are strings. Convert as needed.

Multiple values for one key:

/users?tag=admin&tag=user
const tags = this.route.snapshot.queryParamMap.getAll('tag');
// ['admin', 'user']

Why query params are optional: They’re modifiers. The route works without them. Code that reads them must handle null.

When to use snapshot vs observable:

  • Snapshot — if the query params don’t change while the component is active, or you only care about the initial value.
  • Observable — if the query params can change (like when the user changes a filter), and the component should react.

Common pattern: Subscribe and re-fetch on change.

this.route.queryParamMap.pipe(
  takeUntilDestroyed()
).subscribe(params => {
  const page = Number(params.get('page') ?? '1');
  const sort = params.get('sort') ?? 'name';
  this.loadUsers(page, sort);
});

The component reloads whenever the filter changes.

Why this works: Navigating from /users?page=1 to /users?page=2 reuses the same component. Only the query params change. The observable catches it; the snapshot wouldn’t.

Why query params are read the same way as route params: Consistency. paramMap and queryParamMap share the same API — get, getAll, has. Learn one, learn both. The router treats them uniformly even though they mean different things.


Navigating with parameters

Setting params happens at the navigation call, not by building URLs by hand.

Route params via routerLink:

<a [routerLink]="['/users', userId]">User {{ userId }}</a>

The array ['/users', 42] becomes /users/42.

Route params via navigate:

this.router.navigate(['/users', 42]);

Query params via routerLink:

<a [routerLink]="['/users']" [queryParams]="{ page: 1, sort: 'name' }">
  Users
</a>

Renders /users?page=1&sort=name.

Query params via navigate:

this.router.navigate(['/users'], {
  queryParams: { page: 1, sort: 'name' }
});

Preserving query params:

this.router.navigate(['/users', 42], {
  queryParamsHandling: 'preserve'
});

queryParamsHandling controls what happens to the current query params:

ValueEffect
'merge'Merge new with existing
'preserve'Keep existing, ignore new
'' (default)Replace with new

Replacing query params without navigation:

this.router.navigate([], {
  relativeTo: this.route,
  queryParams: { page: 2 },
  queryParamsHandling: 'merge'
});

Navigating to [] (no path change) with relativeTo keeps the current route but updates the query params. merge keeps existing ones.

Fragment:

this.router.navigate(['/docs'], { fragment: 'section-1' });

Adds #section-1 to the URL.

Why use the API and not strings: Building URLs by hand is error-prone — special characters, encoding, nested paths. The router’s API handles encoding and validation. Use it.

Why queryParamsHandling exists: Query params often carry state that should persist across navigation. preserve keeps them; merge adds to them; the default replaces them. Choosing the right behavior avoids losing filter state when navigating between pages.


A full example

A user list with filtering, pagination, and a detail route.

// ============================================
// USER SERVICE
// ============================================

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  getUsers(filter: { role?: string; page: number; sort: string }): Promise<User[]> {
    const params = new URLSearchParams();
    if (filter.role) params.set('role', filter.role);
    params.set('page', String(filter.page));
    params.set('sort', filter.sort);

    return this.http
      .get<User[]>(`/api/users?${params}`)
      .toPromise() as Promise<User[]>;
  }

  getUser(id: number): Promise<User> {
    return this.http.get<User>(`/api/users/${id}`).toPromise() as Promise<User>;
  }
}

// ============================================
// USER LIST — reads query params
// ============================================

import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h2>Users</h2>

    <div class="filters">
      <label>
        Role:
        <select [value]="role()" (change)="setRole($any($event.target).value)">
          <option value="">All</option>
          <option value="admin">Admin</option>
          <option value="user">User</option>
          <option value="guest">Guest</option>
        </select>
      </label>

      <label>
        Sort:
        <select [value]="sort()" (change)="setSort($any($event.target).value)">
          <option value="name">Name</option>
          <option value="id">ID</option>
        </select>
      </label>
    </div>

    <ul>
      @for (user of users(); track user.id) {
        <li>
          <a [routerLink]="['/users', user.id]"
             [queryParamsHandling]="'preserve'">
            {{ user.name }} ({{ user.role }})
          </a>
        </li>
      }
    </ul>

    <button (click)="prevPage()" [disabled]="page() <= 1">Prev</button>
    <span>Page {{ page() }}</span>
    <button (click)="nextPage()">Next</button>
  `
})
export class UserListComponent {
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  private userService = inject(UserService);

  users = signal<User[]>([]);
  role = signal('');
  page = signal(1);
  sort = signal('name');

  constructor() {
    this.route.queryParamMap
      .pipe(takeUntilDestroyed())
      .subscribe(params => {
        this.role.set(params.get('role') ?? '');
        this.page.set(Number(params.get('page') ?? '1'));
        this.sort.set(params.get('sort') ?? 'name');
        this.loadUsers();
      });
  }

  private loadUsers(): void {
    this.userService
      .getUsers({
        role: this.role() || undefined,
        page: this.page(),
        sort: this.sort()
      })
      .then(users => this.users.set(users));
  }

  setRole(role: string): void {
    this.updateQueryParams({ role: role || null, page: 1 });
  }

  setSort(sort: string): void {
    this.updateQueryParams({ sort });
  }

  prevPage(): void {
    this.updateQueryParams({ page: this.page() - 1 });
  }

  nextPage(): void {
    this.updateQueryParams({ page: this.page() + 1 });
  }

  private updateQueryParams(params: Record<string, string | number | null>): void {
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: params,
      queryParamsHandling: 'merge'
    });
  }
}

// ============================================
// USER DETAIL — reads route params
// ============================================

import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs';

@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `
    @if (user()) {
      <h2>{{ user()!.name }}</h2>
      <p>{{ user()!.email }}</p>
      <p>Role: {{ user()!.role }}</p>
    } @else {
      <p>Loading...</p>
    }
  `
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  private userService = inject(UserService);

  user = signal<User | null>(null);

  constructor() {
    this.route.paramMap
      .pipe(
        takeUntilDestroyed(),
        switchMap(params => {
          const id = Number(params.get('id'));
          return this.userService.getUser(id);
        })
      )
      .subscribe(user => this.user.set(user));
  }
}

// ============================================
// ROUTES
// ============================================

import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: 'users', component: UserListComponent },
  { path: 'users/:id', component: UserDetailComponent }
];

What this shows:

  • UserListComponent reads query params (role, page, sort) via queryParamMap
  • The observable reloads users whenever filters change
  • updateQueryParams navigates with queryParamsHandling: 'merge' to keep existing filters
  • UserDetailComponent reads route params (id) via paramMap
  • switchMap cancels the previous request when the id changes
  • [queryParamsHandling]="'preserve'" on the detail link keeps the filter state when viewing a user

The list reacts to filter changes; the detail reacts to id changes. Both use observables because the params can change.

Why this shape: It’s a real list-detail flow. The list reads filters from the URL so the state is shareable and bookmarkable. The detail reads the id from the URL and reloads on change. Combining both — clicking a user preserves filters — is the natural behavior.


Complete Example Session

# ============================================
# PART 1: BASIC ROUTE PARAM
# ============================================

cat > routes.ts << 'EOF'
import { Routes } from '@angular/router';
import { UserDetailComponent } from './user-detail.component';

export const routes: Routes = [
  { path: 'users/:id', component: UserDetailComponent }
];
EOF

cat > user-detail.component.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user-detail',
  standalone: true,
  template: `<p>User ID: {{ id }}</p>`
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  id = this.route.snapshot.paramMap.get('id');
}
EOF

npx tsc --noEmit user-detail.component.ts
# (no errors)

# ============================================
# PART 2: OBSERVABLE PARAM
# ============================================

cat > reactive.ts << 'EOF'
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-reactive',
  standalone: true,
  template: `<p>User ID: {{ id() }}</p>`
})
export class ReactiveComponent {
  private route = inject(ActivatedRoute);
  id = signal<string | null>(null);

  constructor() {
    this.route.paramMap
      .pipe(takeUntilDestroyed())
      .subscribe(params => {
        this.id.set(params.get('id'));
      });
  }
}
EOF

npx tsc --noEmit reactive.ts
# (no errors)

# ============================================
# PART 3: QUERY PARAMS
# ============================================

cat > query.ts << 'EOF'
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-query',
  standalone: true,
  template: `<p>Search: {{ q() }} Page: {{ page() }}</p>`
})
export class QueryComponent {
  private route = inject(ActivatedRoute);
  q = signal('');
  page = signal(1);

  constructor() {
    this.route.queryParamMap
      .pipe(takeUntilDestroyed())
      .subscribe(params => {
        this.q.set(params.get('q') ?? '');
        this.page.set(Number(params.get('page') ?? '1'));
      });
  }
}
EOF

npx tsc --noEmit query.ts
# (no errors)

# ============================================
# PART 4: NAVIGATE WITH QUERY PARAMS
# ============================================

cat > navigate.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-nav',
  standalone: true,
  template: `<button (click)="search()">Search</button>`
})
export class NavComponent {
  private router = inject(Router);
  private route = inject(ActivatedRoute);

  search(): void {
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: { q: 'hello', page: 1 },
      queryParamsHandling: 'merge'
    });
  }
}
EOF

npx tsc --noEmit navigate.ts
# (no errors)

# ============================================
# PART 5: DATA LOADING PATTERN
# ============================================

cat > data.ts << 'EOF'
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs';

interface User { id: number; name: string; }

@Component({
  selector: 'app-data',
  standalone: true,
  template: `@if (user()) { <p>{{ user()!.name }}</p> }`
})
export class DataComponent {
  private route = inject(ActivatedRoute);
  user = signal<User | null>(null);

  constructor() {
    this.route.paramMap
      .pipe(
        takeUntilDestroyed(),
        switchMap(params => {
          const id = Number(params.get('id'));
          return Promise.resolve({ id, name: `User ${id}` });
        })
      )
      .subscribe(u => this.user.set(u));
  }
}
EOF

npx tsc --noEmit data.ts
# (no errors)

# ============================================
# PART 6: QUERY PARAMS WITH MULTIPLE VALUES
# ============================================

cat > multi.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-multi',
  standalone: true,
  template: `<p>Tags: {{ tags.join(', ') }}</p>`
})
export class MultiComponent {
  private route = inject(ActivatedRoute);
  tags = this.route.snapshot.queryParamMap.getAll('tag');
}
EOF

npx tsc --noEmit multi.ts
# (no errors)

# ============================================
# PART 7: PRESERVE QUERY PARAMS
# ============================================

cat > preserve.ts << 'EOF'
import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-preserve',
  standalone: true,
  template: `<button (click)="go()">Go</button>`
})
export class PreserveComponent {
  private router = inject(Router);
  private route = inject(ActivatedRoute);

  go(): void {
    this.router.navigate(['/users'], {
      queryParams: { page: 2 },
      queryParamsHandling: 'merge'  // keeps existing, adds page
    });
  }
}
EOF

npx tsc --noEmit preserve.ts
# (no errors)

# ============================================
# PART 8: SUMMARY
# ============================================

cat << 'EOF'
Reading data from the URL:

  Route params (path):
    /users/:id
    route.snapshot.paramMap.get('id')
    route.paramMap.subscribe(...)

  Query params (after ?):
    /users?page=2&sort=name
    route.snapshot.queryParamMap.get('page')
    route.queryParamMap.subscribe(...)

Snapshot vs Observable:
  Snapshot    → one-time read
  Observable  → reacts to changes (component reuse)

Navigating with params:
  routerLink         → template
  router.navigate()  → code
  queryParamsHandling → merge / preserve / replace

Conversions:
  paramMap.get()  → string | null
  Number(x)       → number
  x ?? 'default'  → fallback
EOF

Quick Reference

Reading Route Params

MethodReturns
route.snapshot.paramMap.get('id')string | null
route.paramMap.subscribe(...)Observable
params.get('id')string | null
params.getAll('id')string[]
params.has('id')boolean

Reading Query Params

MethodReturns
route.snapshot.queryParamMap.get('q')string | null
route.queryParamMap.subscribe(...)Observable
params.get('q')string | null
params.getAll('tag')string[]

Route Params vs Query Params

AspectRouteQuery
PositionIn pathAfter ?
RequiredYesOptional
Affects routeYesNo
Use forIdentityFilters
Example/users/42/users?page=2
PropertyparamMapqueryParamMap

Snapshot vs Observable

UseWhen
SnapshotOne-time read; params won’t change
ObservableParams may change; component reused
ObservableUnsure
SnapshotSimple case with no reuse

Navigation with Params

FormCode
Static route paramrouterLink="/users/42"
Dynamic route param[routerLink]="['/users', 42]"
Query params[queryParams]="{ page: 1 }"
Fragmentfragment="section"
Programmatic routerouter.navigate(['/users', 42])
Programmatic queryrouter.navigate(['/users'], { queryParams })

queryParamsHandling

ValueEffect
'' (default)Replace all
'merge'Merge new with existing
'preserve'Keep existing, ignore new

Type Conversions

InputOutput
paramMap.get('id')string | null
Number(x)number
x ?? 'default'string
x?.toUpperCase()string | undefined

Common Patterns

PatternCode
Read oncesnapshot.paramMap.get(...)
React to changeparamMap.pipe(takeUntilDestroyed()).subscribe(...)
Load on changeparamMap.pipe(switchMap(...))
Convert to signaltoSignal(route.paramMap.pipe(map(...)))
Update queryrouter.navigate([], { queryParams, relativeTo: route })
Merge queriesqueryParamsHandling: 'merge'

Parsing Numbers

TaskCode
Safe numberNumber(param ?? '0')
With defaultNumber(param ?? '1')
Fallback stringparam ?? 'default'
Null checkif (param !== null) { }

Route Config with Params

ConfigURL
{ path: 'users/:id' }/users/42
{ path: 'users/:userId/posts/:postId' }/users/1/posts/2
{ path: ':category/:id' }/books/42
{ path: 'files/*' }/files/a/b/c (wildcard)

Error Cases

ErrorCause
Cannot read property of nullSnapshot returned null
Stale valueUsed snapshot with changing param
NaNNumber(null) or Number('abc')
Route not matchedMissing param in config

Related APIs

APIPurpose
ActivatedRouteAccess current route
paramMapRoute params
queryParamMapQuery params
snapshotCurrent value
RouterNavigate programmatically
RouterLinkNavigate declaratively

Best Practices

Do This:

// Use observable when params may change
this.route.paramMap.pipe(
  takeUntilDestroyed()
).subscribe(params => {
  const id = params.get('id');
});                                                              // ✅

// Use snapshot for one-time reads
const id = this.route.snapshot.paramMap.get('id');               // ✅

// Convert strings to numbers
const id = Number(this.route.snapshot.paramMap.get('id'));       // ✅

// Provide defaults for query params
const page = Number(this.route.snapshot.queryParamMap.get('page') ?? '1'); // ✅

// Use switchMap for data loading on param change
this.route.paramMap.pipe(
  switchMap(params => this.service.get(Number(params.get('id'))))
).subscribe();                                                   // ✅

// Preserve query params when navigating
this.router.navigate(['/users', id], { queryParamsHandling: 'preserve' }); // ✅

// Merge query params when updating one
this.router.navigate([], {
  relativeTo: this.route,
  queryParams: { page: 2 },
  queryParamsHandling: 'merge'
});                                                              // ✅

// Use query params for filters
// Route params for identity                                      // ✅

// Combine with signals
id = toSignal(this.route.paramMap.pipe(map(p => p.get('id'))));  // ✅

Don’t Do This:

// Don't use snapshot when params change
const id = this.route.snapshot.paramMap.get('id');
// on /users/1 → /users/2, id stays '1'                          // ❌

// Don't use Number without a default
const page = Number(this.route.snapshot.queryParamMap.get('page'));
// → 0, not 1, when missing                                      // ⚠️

// Don't build URLs by hand
this.router.navigateByUrl(`/users/${id}?page=${p}`);              // ⚠️

// Don't forget queryParamsHandling
this.router.navigate(['/users'], { queryParams: { page: 2 } });
// drops other query params                                      // ⚠️

// Don't subscribe without cleanup when not a route observable
someObservable.subscribe(...);  // ⚠️  leaks                    // ⚠️

// Don't access params synchronously before activation
// route.snapshot works; route.params does not yet                // ⚠️

// Don't parse without fallback for missing params
const role = this.route.snapshot.queryParamMap.get('role');
if (role.length > 0) { }  // ❌ null if missing                  // ❌

// Don't use query params for required data
// /users (no id) → the route can't identify which user          // ⚠️

Common Pitfalls

PitfallProblemSolution
Snapshot with changing paramsStale valueUse observable
Number(null)0Provide default
Missing query paramnullFallback with ??
Forgot queryParamsHandlingLost filtersUse merge
Assuming param is a numberCompile errorConvert with Number
Not converting from stringType mismatchParse explicitly
Ignoring param changesSame component reusedSubscribe
Building URLs manuallyEncoding issuesUse router API
Missing param in route configRoute doesn’t matchAdd :name
Multiple valuesOnly first returnedUse getAll

Real-World Examples

1. Route param from path

this.route.snapshot.paramMap.get('id');

2. Multiple route params

const userId = params.get('userId');
const postId = params.get('postId');

3. Query param

this.route.snapshot.queryParamMap.get('q');

4. Query with default

const page = Number(this.route.snapshot.queryParamMap.get('page') ?? '1');

5. Reactive route param

this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => {
  this.load(params.get('id'));
});

6. Reactive query param

this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe(params => {
  this.filter(params.get('role'));
});

7. Switch map on route param

this.route.paramMap.pipe(
  switchMap(p => this.service.get(Number(p.get('id'))))
).subscribe();

8. Route param via link

<a [routerLink]="['/users', user.id]">User {{ user.id }}</a>

9. Query param via link

<a [routerLink]="['/search']" [queryParams]="{ q: term }">Search</a>

10. Navigate with query params

this.router.navigate(['/users'], { queryParams: { page: 2 } });

11. Merge query params

this.router.navigate([], {
  relativeTo: this.route,
  queryParams: { page: 2 },
  queryParamsHandling: 'merge'
});

12. Preserve query params

this.router.navigate(['/users', id], { queryParamsHandling: 'preserve' });

13. Get all values

const tags = this.route.snapshot.queryParamMap.getAll('tag');

14. Convert to signal

id = toSignal(this.route.paramMap.pipe(map(p => p.get('id'))));

15. Load data on change

constructor() {
  this.route.paramMap.pipe(
    takeUntilDestroyed(),
    switchMap(p => this.service.get(Number(p.get('id'))))
  ).subscribe(d => this.data.set(d));
}

16. Query param toggle

toggleView(): void {
  this.router.navigate([], {
    relativeTo: this.route,
    queryParams: { view: 'grid' },
    queryParamsHandling: 'merge'
  });
}

17. Reset pagination on filter change

setFilter(role: string): void {
  this.router.navigate([], {
    relativeTo: this.route,
    queryParams: { role, page: 1 },
    queryParamsHandling: 'merge'
  });
}

18. Fragment navigation

this.router.navigate(['/docs'], { fragment: 'install' });

19. Check param presence

if (params.has('role')) {
  const role = params.get('role');
}

20. Fallback for missing route param

const id = Number(this.route.snapshot.paramMap.get('id') ?? '0');
if (id === 0) {
  this.router.navigate(['/404']);
}

Visual: URL Structure

┌───────────────────────────────────────────────┐
│  /users/42?tab=posts&page=2#section           │
│  ─────┬───┬─ ────────────┬─────── ──┬────     │
│       │   │              │          │         │
│       │   │              │          └ fragment│
│       │   │              │                    │
│       │   │              └ query params       │
│       │   │                (tab, page)        │
│       │   │                                   │
│       │   └ route param                       │
│       │     (id = 42)                         │
│       │                                       │
│       └ path                                  │
│                                               │
└───────────────────────────────────────────────┘

Visual: Route Param vs Query Param

┌──────────────────────────────────────────────┐
│  /users/42                                   │
│         └── route param (id)                 │
│                                              │
│  Identifies a specific user                  │
│  Required to render                          │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  /users?page=2&sort=name                     │
│         └── query params                     │
│                                              │
│  Modifies how the list is shown              │
│  Optional                                    │
│                                              │
└──────────────────────────────────────────────┘

Visual: Snapshot vs Observable

┌──────────────────────────────────────────────┐
│  URL: /users/1                               │
│  Component created                           │
│                                              │
│  Snapshot: '1'                               │
│  Observable emits: '1'                       │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  │  navigate /users/2
                  ▼
┌──────────────────────────────────────────────┐
│  Component reused (not recreated)            │
│                                              │
│  Snapshot: '1' ← still the old value         │
│  Observable emits: '2' ← updated ✅          │
│                                              │
└──────────────────────────────────────────────┘

Visual: Reading Params

┌──────────────────────────────────────────────┐
│  ActivatedRoute                              │
│                                              │
│  ├── snapshot                                │
│  │   ├── paramMap                            │
│  │   │   └── get('id')                       │
│  │   └── queryParamMap                       │
│  │       └── get('page')                     │
│  │                                           │
│  ├── paramMap (observable)                   │
│  │   └── subscribe(...)                      │
│  │                                           │
│  └── queryParamMap (observable)              │
│      └── subscribe(...)                      │
│                                              │
└──────────────────────────────────────────────┘

Visual: Data Loading on Param Change

┌──────────────────────────────────────────────┐
│  URL changes: /users/1 → /users/2            │
│       │                                      │
│       ▼                                      │
│  paramMap emits new value                    │
│       │                                      │
│       ▼                                      │
│  switchMap cancels previous request          │
│       │                                      │
│       ▼                                      │
│  Service loads new data                      │
│       │                                      │
│       ▼                                      │
│  Signal updated → view rerenders             │
│                                              │
└──────────────────────────────────────────────┘

Visual: Query Param Update

┌──────────────────────────────────────────────┐
│  Current URL: /users?page=1&sort=name        │
│                                              │
│  User clicks "Next"                          │
│       │                                      │
│       ▼                                      │
│  router.navigate([], {                       │
│    relativeTo: this.route,                   │
│    queryParams: { page: 2 },                 │
│    queryParamsHandling: 'merge'              │
│  })                                          │
│                                              │
│  Result: /users?page=2&sort=name             │
│  (sort preserved, page updated)              │
│                                              │
└──────────────────────────────────────────────┘

Visual: queryParamsHandling

┌──────────────────────────────────────────────┐
│  Current: ?page=1&sort=name                  │
│  New:     { page: 2 }                        │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  '' (default):                               │
│  Result: ?page=2                             │
│  (sort lost)                                 │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  'merge':                                    │
│  Result: ?page=2&sort=name                   │
│  (sort kept, page replaced)                  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  'preserve':                                 │
│  Result: ?page=1&sort=name                   │
│  (new ignored, old kept)                     │
│                                              │
└──────────────────────────────────────────────┘

Visual: Type Conversions

┌──────────────────────────────────────────────┐
│  paramMap.get('id')                          │
│       │                                      │
│       ▼                                      │
│  string | null                               │
│       │                                      │
│       ├── as-is    → string                  │
│       ├── Number() → number | NaN            │
│       ├── ?? 0     → string                  │
│       └── parseInt → number                  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Safe conversion:                            │
│                                              │
│  Number(params.get('id') ?? '0')             │
│                                              │
│  → 0 if missing                              │
│  → NaN if non-numeric                        │
│                                              │
└──────────────────────────────────────────────┘

Visual: Component Reuse

┌──────────────────────────────────────────────┐
│  URL: /users/1                               │
│       │                                      │
│       ▼                                      │
│  Create UserDetailComponent                  │
│       │                                      │
│       ▼                                      │
│  ngOnInit runs once                          │
│                                              │
└──────────────────────────────────────────────┘
                  │
                  │  navigate /users/2
                  ▼
┌──────────────────────────────────────────────┐
│  Angular matches same route config           │
│       │                                      │
│       ▼                                      │
│  Reuses UserDetailComponent                  │
│       │                                      │
│       ▼                                      │
│  ngOnInit does NOT run again                 │
│       │                                      │
│       ▼                                      │
│  Only param observable fires ✅              │
│                                              │
└──────────────────────────────────────────────┘

Visual: Decision Flow

┌──────────────────────────────────────────────┐
│  Reading a param?                            │
│       │                                      │
│       ├── Will it change?                    │
│       │      │                               │
│       │      ├── Yes ──► subscribe to paramMap│
│       │      │                               │
│       │      └── No  ──► snapshot            │
│       │                                      │
│       └── Unsure ──► subscribe               │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Which param type?                           │
│       │                                      │
│       ├── Identity (required) ──► route param│
│       │                                      │
│       └── Filter (optional) ──► query param  │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Navigating with params?                     │
│       │                                      │
│       ├── Template ──► routerLink            │
│       │                                      │
│       ├── Code ──► router.navigate           │
│       │                                      │
│       └── Update query only ──► navigate([], │
│           { queryParamsHandling: 'merge' })  │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptMeaning
Route paramPart of the path — /users/:id
Query paramAfter ?/users?page=2
paramMapRoute params API
queryParamMapQuery params API
SnapshotValue at activation
ObservableEmits on change
queryParamsHandlingMerge, preserve, replace
getAllMultiple values
getFirst value

Key takeaways:

  • Route params are part of the path — :id matches a segment
  • Query params are after ? — optional modifiers
  • Read both through ActivatedRouteparamMap and queryParamMap
  • Use snapshot for one-time reads; use the observable when params may change
  • Angular reuses the component when only params change — snapshot goes stale
  • get returns string | null; getAll returns string[]
  • Param values are strings — convert with Number() and provide defaults with ??
  • Navigate with params via [routerLink]="['/users', id]" or router.navigate(['/users', id])
  • Query params go in { queryParams: {...} }
  • queryParamsHandling: 'merge' keeps existing query params when adding new ones
  • preserve keeps the existing ones and ignores the new
  • Use switchMap to load data on every param change
  • Route params identify a resource; query params configure the view
  • Combine with signals via toSignal for reactive param reads

Remember: URLs carry state. Route params identify which resource; query params configure how it’s shown. Read them from ActivatedRoute — but pick the right tool: snapshot for one-time reads, observable when params may change. Angular reuses components, so snapshot goes stale. The observable is the safe default. Navigate with the router’s API, not hand-built strings. And use queryParamsHandling: 'merge' when updating filters so you don’t lose the rest of the state. That’s the whole craft of parameterized routes.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!