Angular Router Essentials: routerLink, RouterOutlet, Params, Query Params [2026]

Link copied
Angular Router Essentials: routerLink, RouterOutlet, Params, Query Params [2026]

Angular Router Essentials: routerLink, RouterOutlet, Params, Query Params [2026]

Angular Tutorial Module 5: Routing Lesson 5.1

Every Angular app with more than one page goes through the Router. It maps URLs to components, handles navigation, parses params, and lets your code react to route changes. This lesson is the on-ramp: a Routes config, a <router-outlet>, navigation by link and by code, reading params, and the few mechanics that catch newcomers.

This is lesson 5.1 of the Angular Tutorial, opening Module 5. After this lesson you can ship a multi-page Angular app. Lessons 5.2–5.6 dive into the rest of the router surface — complete route configuration, guards, events, resolvers, and lazy loading.

The shape #

A router-enabled app needs three things:

  1. provideRouter(routes) in app.config.ts (lesson 1.6)
  2. A routes array declaring path→component mappings
  3. A <router-outlet /> somewhere in your template
// app.routes.ts
import { Routes } from '@angular/router';
import { HomePage } from './pages/home';
import { AboutPage } from './pages/about';
import { NotFoundPage } from './pages/not-found';

export const routes: Routes = [
  { path: '',         component: HomePage },
  { path: 'about',    component: AboutPage },
  { path: '**',       component: NotFoundPage },
];
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes)],
};
// app.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/">Home</a>
      <a routerLink="/about">About</a>
    </nav>
    <router-outlet />
  `,
})
export class App {}

Three imports per template that uses the router: RouterOutlet for the slot, RouterLink for declarative navigation, and (later) RouterLinkActive for active-link styling.

<router-outlet /> — where the routed component renders #

When the URL matches a route, the matching component renders INSIDE the <router-outlet />. The outlet is a placeholder — the routed component appears next to it (as a sibling), not inside it.

A typical layout:

<app-root>
  <header>...</header>
  <main>
    <router-outlet />
    <!-- routed component renders here, as a sibling of router-outlet -->
  </main>
  <footer>...</footer>
</app-root>

For nested layouts (a feature shell that itself has sub-pages), nest <router-outlet> inside a routed component — that's how nested routes work (covered with children: config in lesson 5.2).

Use routerLink instead of href for in-app links. It prevents full-page reloads and works with the router's history.

<a routerLink="/about">About</a>
<a routerLink="/users/42">User 42</a>
<a [routerLink]="['/users', userId()]">User {{ userId() }}</a>
<a [routerLink]="['/posts', postId(), 'edit']">Edit post</a>

Three forms:

  • String form: routerLink="/foo" — a literal path
  • Array form: [routerLink]="['/users', id]" — segments concatenated with /
  • Relative form: [routerLink]="['edit']" — relative to the current route

The array form is what you reach for whenever any segment is dynamic.

Apply a CSS class when the link's target matches the current URL:

<a routerLink="/about" routerLinkActive="active">About</a>

For exact-match-only (link active only when URL is exactly /about, not /about/subpage):

<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>

The exact: true option is almost always what you want for the homepage link, because routerLink="/" would otherwise be "active" on every page (every URL starts with /).

Path parameters #

Declare placeholders with :name syntax:

export const routes: Routes = [
  { path: 'users/:id',         component: UserDetailPage },
  { path: 'posts/:id/edit',    component: PostEditPage },
];

The component reads them via ActivatedRoute:

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

@Component({
  selector: 'app-user-detail',
  template: `<h1>User {{ id() }}</h1>`,
})
export class UserDetailPage {
  private route = inject(ActivatedRoute);
  // params emits whenever ANY param changes; .pipe(map) extracts the one we want
  id = toSignal(this.route.params.pipe(map(p => p['id'])));
}

The cleaner v17+ alternative is withComponentInputBinding() — covered next.

withComponentInputBinding() — params as inputs #

When you enable it on the router, every route parameter is bound to a matching input() on the component:

import { provideRouter, withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
  ],
};

Now:

@Component({
  template: `<h1>User {{ id() }}</h1>`,
})
export class UserDetailPage {
  id = input.required<string>();
  // ↑ automatically receives the value of :id from the URL
}

No ActivatedRoute injection, no toSignal(), no pipe(map(...)). The router writes path params, query params, and the route's data directly into matching component inputs. This is the modern v17+ pattern and what you should use in new code.

For programmatic navigation (after a form submit, after auth check):

import { Router, inject } from '@angular/router';

export class LoginComponent {
  private router = inject(Router);

  onSubmit() {
    // String form
    this.router.navigateByUrl('/dashboard');

    // Array form (preferred for dynamic segments)
    this.router.navigate(['/users', this.userId()]);

    // With query params
    this.router.navigate(['/search'], { queryParams: { q: 'angular' } });

    // Replace history (no back-button entry)
    this.router.navigate(['/login'], { replaceUrl: true });
  }
}

The array form has the same semantics as [routerLink] array — segments separated by /, dynamic values mixed in.

Query parameters #

Declare links with query params:

<a [routerLink]="['/search']" [queryParams]="{ q: 'angular', sort: 'date' }">
  Search
</a>

Read them with withComponentInputBinding():

@Component({...})
export class SearchPage {
  q = input<string | undefined>();           // ?q=...
  sort = input<string | undefined>();         // ?sort=...
}

Query params are reactive — when the URL changes (a different ?q=), the input signal updates and the component reacts.

Wildcard route — 404 catch-all #

The ** path matches any URL not matched by an earlier route. Always last in the array:

export const routes: Routes = [
  { path: '', component: HomePage },
  { path: 'about', component: AboutPage },
  { path: '**', component: NotFoundPage },   // matches anything else
];

For a custom 404, route to a NotFoundPage. To redirect 404s back home:

{ path: '**', redirectTo: '/', pathMatch: 'full' }

The pathMatch: 'full' is required when redirectTo is on a non-empty path — covered in lesson 5.2.

Fragment (URL hash) #

Links with #fragment:

<a routerLink="/about" fragment="team">About — team section</a>

Produces /about#team. The router supports withInMemoryScrolling({ scrollPositionRestoration: 'enabled', anchorScrolling: 'enabled' }) to auto-scroll to the matching id on navigation — covered in lesson 5.4 (router events).

A complete tiny app #

// app.routes.ts
import { Routes } from '@angular/router';
import { HomePage } from './pages/home';
import { UserListPage } from './pages/user-list';
import { UserDetailPage } from './pages/user-detail';
import { NotFoundPage } from './pages/not-found';

export const routes: Routes = [
  { path: '',           component: HomePage,       title: 'Home' },
  { path: 'users',      component: UserListPage,   title: 'Users' },
  { path: 'users/:id',  component: UserDetailPage, title: 'User detail' },
  { path: '**',         component: NotFoundPage,   title: 'Not found' },
];

// app.config.ts
import { provideRouter, withComponentInputBinding } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withComponentInputBinding())],
};

// app.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet, RouterLink, RouterLinkActive],
  template: `
    <nav>
      <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
      <a routerLink="/users" routerLinkActive="active">Users</a>
    </nav>
    <main>
      <router-outlet />
    </main>
  `,
  styles: `nav a.active { font-weight: bold; }`,
})
export class App {}

// pages/user-detail.ts
import { Component, input } from '@angular/core';

@Component({
  template: `<h1>User {{ id() }}</h1>`,
})
export class UserDetailPage {
  id = input.required<string>();   // bound from :id in URL
}

Four routes, dynamic path param, programmatic-input binding, active-link styling, wildcard 404 — about 40 lines.

The title property #

When you set title: 'Page name' on a route, the router updates document.title automatically. For dynamic titles:

{ path: 'users/:id', component: UserDetailPage, title: (route) => `User ${route.paramMap.get('id')}` }

Replaces the manual document.title = 'X' calls of older Angular code.

Common gotchas #

Symptom Cause Fix
<router-outlet> not rendering Missing RouterOutlet in component's imports array Add it
routerLink does nothing Missing RouterLink in imports, or the path doesn't match any route Check both
routerLinkActive matches too much Default behavior matches sub-paths Add [routerLinkActiveOptions]="{ exact: true }"
:id input is undefined Forgot withComponentInputBinding() Add it to provideRouter(...) features
Navigation triggers a full page reload Used href instead of routerLink Switch to routerLink for in-app navigation
Wildcard route never matches Wildcard isn't LAST in the routes array Move { path: '**', ... } to the end

What's next #

Lesson 5.2 covers complete route configuration — every property on the Route object (pathMatch, redirectTo, children, outlet, data, resolve, etc.). Lesson 5.3 walks functional route guards (CanActivateFn, CanDeactivateFn). Lesson 5.4 catalogs router events. Lesson 5.5 covers data resolvers and prefetching. Lesson 5.6 closes Module 5 with lazy loading and @defer blocks.

Try it yourself #

A two-page app with dynamic routing:

import { Component, input } from '@angular/core';
import { provideRouter, withComponentInputBinding, RouterOutlet, RouterLink } from '@angular/router';
import { bootstrapApplication } from '@angular/platform-browser';

const Posts = [{id:1,title:'First'},{id:2,title:'Second'},{id:3,title:'Third'}];

@Component({
  imports: [RouterLink],
  template: `
    <h1>Posts</h1>
    <ul>@for (p of posts; track p.id) { <li><a [routerLink]="['/posts', p.id]">{{ p.title }}</a></li> }</ul>
  `,
})
class ListPage { posts = Posts; }

@Component({
  template: `<h1>{{ post()?.title }}</h1><a routerLink="/">Back</a>`,
  imports: [RouterLink],
})
class DetailPage {
  id = input.required<string>();
  post = computed(() => Posts.find(p => p.id === +this.id()));
}

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `<router-outlet />`,
})
class App {}

bootstrapApplication(App, {
  providers: [provideRouter([
    { path: '', component: ListPage },
    { path: 'posts/:id', component: DetailPage },
  ], withComponentInputBinding())],
});

Four pages, dynamic param binding, programmatic-input wiring — in a single file.

YouShould I always use withComponentInputBinding(), or only sometimes?
YouAlways for new apps. It’s a single line in app.config.ts, makes path/query params and route data flow into components as input() signals, and replaces the older ActivatedRoute.params.pipe(...) dance. Cost: zero unless you have collision between an input name and a route param name (rare). Migration story: legacy ActivatedRoute usage continues to work — opt in to withComponentInputBinding and migrate components opportunistically.

Lesson 5.2 picks up the complete route configuration — every property on the Route object.

Up next in Angular

More from this topic

View all Angular articles →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *