Angular Router Navigation: ActivatedRoute, Query Params, Route Data and Router Events

Angular Tutorial: Beginner to Advanced · 9. Routing and navigation · 5 min read · Updated

You already know routes and parameters. This lesson fills the gaps: moving around from code, reading everything the URL can carry, highlighting the active link and reacting to navigation.

What you will learn

  • RouterLink and Router.navigate
  • ActivatedRoute: params, query params, fragment and static data
  • RouterLinkActive for the active menu item
  • Router events and how to get the current route

In a template use routerLink. In code inject Router:

// src/app/nav/nav.routes.ts
import { Routes } from '@angular/router';
import { ProductsPage } from './products-page';
import { ProductDetail } from './product-detail';

export const NAV_ROUTES: Routes = [
  { path: 'shop', component: ProductsPage, title: 'Shop', data: { section: 'catalog' } },
  { path: 'shop/:id', component: ProductDetail, title: 'Product', data: { section: 'catalog' } },
];
// src/app/nav/products-page.ts
import { Component, inject } from '@angular/core';
import { Router, RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-products-page',
  imports: [RouterLink, RouterLinkActive],
  template: `
    <nav>
      <a routerLink="/shop" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">All</a>
      <a [routerLink]="['/shop', 7]" routerLinkActive="active">Product 7</a>
    </nav>
    <button type="button" (click)="open(9)">Open 9 (from code)</button>
    <button type="button" (click)="search('shoes')">Search shoes</button>
  `,
})
export class ProductsPage {
  private readonly router = inject(Router);

  open(id: number) {
    // same as /shop/9
    this.router.navigate(['/shop', id]);
  }

  search(term: string) {
    // /shop?q=shoes#results   (query params and a fragment)
    this.router.navigate(['/shop'], { queryParams: { q: term }, fragment: 'results' });
  }
}

Points to remember:

  • The array is path segments: ['/shop', id] becomes /shop/9. Do not build strings by hand.
  • navigate returns a Promise<boolean>. Use navigateByUrl('/shop/9') when you already have a full URL string.
  • routerLinkActive adds a CSS class when the link matches the current URL. Add exact: true to the home link, otherwise / is active for every page.
  • Relative navigation: this.router.navigate(['edit'], { relativeTo: this.route }).

ActivatedRoute: reading the URL

ActivatedRoute describes the route that created your component. Its properties are Observables, because the same component can be reused when only the parameter changes (/shop/1 to /shop/2).

// src/app/nav/product-detail.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-product-detail',
  template: `
    <h2>Product {{ id() }}</h2>
    <p>Section: {{ section }}</p>
    <p>Search: {{ q() ?? 'none' }}</p>
  `,
})
export class ProductDetail {
  private readonly route = inject(ActivatedRoute);

  id = toSignal(this.route.paramMap.pipe(map((p) => p.get('id'))));
  q = toSignal(this.route.queryParamMap.pipe(map((p) => p.get('q'))));
  section = this.route.snapshot.data['section']; // static data never changes
}

Which one do I use?

  • paramMap: the :id parts of the path.
  • queryParamMap: the part after ?.
  • fragment: the part after #.
  • data: fixed values you attach in the route config (a role, a layout name, a page title).
  • snapshot: the value right now. Good for data that cannot change while the component lives.

### The simplest way: component input binding

If you enable withComponentInputBinding() in provideRouter, params, query params and data become normal signal inputs with the same name. No ActivatedRoute needed:

// File: with withComponentInputBinding()
id = input<string>();   // from :id
q = input<string>();    // from ?q=
section = input<string>(); // from data

Router events and the current route

The router tells you what is happening through router.events:

// src/app/nav/route-logger.ts
import { Component, DestroyRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, NavigationStart, Router } from '@angular/router';
import { filter } from 'rxjs';

@Component({
  selector: 'app-route-logger',
  template: `
    @if (loading()) { <p>Loading page...</p> }
    <p>Current URL: {{ url() }}</p>
  `,
})
export class RouteLogger {
  private readonly router = inject(Router);
  loading = signal(false);
  url = signal(this.router.url);

  constructor() {
    this.router.events.pipe(takeUntilDestroyed()).subscribe((event) => {
      if (event instanceof NavigationStart) this.loading.set(true);
    });
    this.router.events
      .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd), takeUntilDestroyed())
      .subscribe((e) => {
        this.loading.set(false);
        this.url.set(e.urlAfterRedirects);
      });
  }
}

Useful events in order: NavigationStart, RoutesRecognized, NavigationEnd (success), NavigationCancel (a guard said no) and NavigationError.

How to get the current route? Use router.url for the URL string, route.snapshot for the params, or router.events filtered by NavigationEnd when you need to react to changes.

Test it

// src/app/nav/nav.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { NAV_ROUTES } from './nav.routes';

describe('navigation', () => {
  beforeEach(() => TestBed.configureTestingModule({ providers: [provideRouter(NAV_ROUTES)] }));

  it('reads the param, query param and route data', async () => {
    const harness = await RouterTestingHarness.create();
    await harness.navigateByUrl('/shop/42?q=shoes');
    const el = harness.routeNativeElement as HTMLElement;
    expect(el.textContent).toContain('Product 42');
    expect(el.textContent).toContain('Search: shoes');
    expect(el.textContent).toContain('Section: catalog');
  });

  it('navigates from code', async () => {
    const harness = await RouterTestingHarness.create('/shop');
    const router = TestBed.inject((await import('@angular/router')).Router);
    await router.navigate(['/shop', 9]);
    expect(router.url).toBe('/shop/9');
  });
});

Common mistakes

  • Reading snapshot.paramMap once in a component that is reused for different ids.
  • Forgetting to unsubscribe from router.events. takeUntilDestroyed() does it for you.
  • Missing exact: true on the home link.
  • Building URLs with string concatenation instead of the array form.

Try it yourself

  1. Add a link that keeps the current query params with queryParamsHandling: 'merge'.
  2. Show a small loading bar using NavigationStart and NavigationEnd.
  3. Move section to withComponentInputBinding() and delete ActivatedRoute.

Quick check

  1. What is the difference between paramMap and queryParamMap?
  2. When is snapshot enough?
  3. Which event fires when a guard blocks navigation?

Next, continue with Angular Route Guards: Protect Pages with CanActivateFn.

Frequently asked questions

Why are route params Observables?

Angular reuses a component when only the parameter changes. The Observable emits the new value so you can react. A snapshot is a one time copy.

What is the difference between navigate and navigateByUrl?

navigate takes an array of path segments and options, navigateByUrl takes one complete URL string.