Angular's router has three core pieces: routes (which component shows at which URL), outlets (where the component renders in the template), and links (how users navigate without a full page reload).
1. Setup
app.routes.ts
Define your routes in a dedicated file. Angular uses first-match wins, so order matters — put more specific routes before general ones. Unlike React Router (v6+), which automatically ranks routes by specificity regardless of order, Angular matches top-to-bottom and stops at the first hit:
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { TasksComponent } from './tasks/tasks.component';
import { NotFoundComponent } from './not-found/not-found.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'tasks', component: TasksComponent },
{ path: '**', component: NotFoundComponent }, // wildcard — must be last
];
app.config.ts
Pass the routes to provideRouter in your app config:
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
],
};
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig);
2. <router-outlet>
Place <router-outlet /> in your root component's template where matched components should render:
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
template: `
<nav>...</nav>
<router-outlet />
`,
})
export class AppComponent {}
3. routerLink — Navigation Links
Use routerLink instead of href to navigate without a full page reload:
import { RouterLink } from '@angular/router';
@Component({
imports: [RouterLink],
template: `
<a routerLink="/tasks">Tasks</a>
`,
})
export class NavComponent {}
routerLink also accepts an array, which is useful for building dynamic links:
<!-- static -->
<a [routerLink]="['/tasks']">Tasks</a>
<!-- with a dynamic param -->
<a [routerLink]="['/tasks', task.id]">{{ task.title }}</a>
Paths starting with / are absolute — they always navigate from the app root. Paths without a leading slash are relative — they resolve from the current route:
<!-- absolute: always goes to /tasks -->
<a routerLink="/tasks">Tasks</a>
<!-- relative: from /users/123 → /users/123/tasks -->
<a routerLink="tasks">Tasks</a>
<!-- go up one level: from /users/123 → /users -->
<a routerLink="..">Back</a>
For programmatic relative navigation, pass relativeTo to Router.navigate():
// from /users/123 → /users/123/edit
this.router.navigate(['edit'], { relativeTo: this.route });
// go up one level
this.router.navigate(['..'], { relativeTo: this.route });
Active link styling with RouterLinkActive
RouterLinkActive adds a CSS class to the element when its route is active:
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
imports: [RouterLink, RouterLinkActive],
template: `
<a routerLink="/tasks" routerLinkActive="active-link">Tasks</a>
`,
})
export class NavComponent {}
For the root path, use [routerLinkActiveOptions]="{ exact: true }" to prevent it from matching every route:
<a routerLink="/" routerLinkActive="active-link" [routerLinkActiveOptions]="{ exact: true }">Home</a>
4. Route Parameters
Define a dynamic segment with :paramName:
{ path: 'tasks/:id', component: TaskDetailComponent }
Option A — ActivatedRoute
Inject ActivatedRoute and subscribe to its params or paramMap observable.
params gives a plain object — simple, but less safe:
this.route.params.subscribe(params => {
this.taskId.set(params['id']);
});
paramMap is preferred — it provides helper methods (get(), has(), getAll()) and handles multi-value params:
import { Component, inject, signal, DestroyRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ ... })
export class TaskDetailComponent {
taskId = signal('');
private route = inject(ActivatedRoute);
constructor() {
this.route.paramMap
.pipe(takeUntilDestroyed())
.subscribe(params => {
this.taskId.set(params.get('id') ?? '');
});
}
}
Use
takeUntilDestroyed()to automatically unsubscribe when the component is destroyed. Call it inside the constructor (or any injection context) — it needs access toDestroyRefinternally.
Option B — input() binding (Angular 16+)
Since Angular 16 you can bind route params directly to component inputs. Enable it once in app.config.ts:
import { withComponentInputBinding } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
],
};
Then declare an input whose name matches the route param — no ActivatedRoute needed:
import { Component, input } from '@angular/core';
@Component({ ... })
export class TaskDetailComponent {
id = input<string>(); // automatically set to the :id param
}
This also works with @Input() for class-based style. Prefer input() signals for new code.
Redirect routes
Redirect one path to another using redirectTo. Use pathMatch: 'full' on the empty path so it doesn't match every route:
{ path: '', redirectTo: '/tasks', pathMatch: 'full' },
5. Programmatic Navigation
Inject the Router service to navigate from code (e.g. after a form submit):
import { Router } from '@angular/router';
@Component({ ... })
export class NewTaskComponent {
private router = inject(Router);
onSubmit() {
// ... save task
this.router.navigate(['/tasks']);
// or with a param:
this.router.navigate(['/tasks', taskId]);
}
}
navigateByUrl is an alternative when you have a full URL string:
this.router.navigateByUrl('/tasks');
6. Page Titles
Set the browser tab title per route with the title property:
{ path: 'tasks', component: TasksComponent, title: 'My Tasks' },
The title can also be a resolver function for dynamic titles:
{
path: 'tasks/:id',
component: TaskDetailComponent,
title: (route: ActivatedRouteSnapshot) => `Task ${route.paramMap.get('id')}`,
}
7. Nested Routes
Use children to nest routes under a parent. The parent component must contain its own <router-outlet /> where child components render.
export const routes: Routes = [
{
path: 'users/:userId',
component: UserShellComponent, // renders the layout, contains <router-outlet />
children: [
{ path: '', component: UserDetailComponent }, // /users/42
{ path: 'edit', component: UserEditComponent }, // /users/42/edit
],
},
];
UserShellComponent holds the shared layout (e.g. a header with the user's name) and a <router-outlet /> for the active child:
@Component({
imports: [RouterOutlet],
template: `
<h2>User Profile</h2>
<router-outlet />
`,
})
export class UserShellComponent {}
Child components can read the :userId param from ActivatedRoute directly — the router provides the parent route's params to all child routes:
@Component({ ... })
export class UserDetailComponent {
private route = inject(ActivatedRoute);
constructor() {
this.route.paramMap
.pipe(takeUntilDestroyed())
.subscribe(params => {
const userId = params.get('userId');
// load user...
});
}
}
8. snapshot vs Observable
ActivatedRoute exposes two ways to read route state:
-
snapshot— a one-time read of the current route. Fast and simple, but not reactive. -
paramMapobservable — emits every time the route changes. Required when the component stays mounted while params change.
The trap with snapshot: if you navigate from /users/1 to /users/2 and the UserDetailComponent is reused (not destroyed), the snapshot stays frozen at the first value and your UI won't update.
// ❌ snapshot — only reads once, misses future param changes
const userId = this.route.snapshot.paramMap.get('userId');
// ✓ observable — reacts to every navigation
this.route.paramMap
.pipe(takeUntilDestroyed())
.subscribe(params => {
const userId = params.get('userId');
// load user...
});
Use snapshot only when you know the component is always destroyed and recreated on navigation (i.e. it's not a reused route). In all other cases, subscribe to the observable.
9. Query Params
Query params are the ?key=value part of the URL (e.g. /tasks?order=asc). Unlike route params, they are optional and don't affect which route is matched.
Setting query params
Via routerLink using [queryParams]:
<a routerLink="." [queryParams]="{ order: 'asc' }">Sort ascending</a>
Or programmatically:
this.router.navigate(['/tasks'], { queryParams: { order: 'asc' } });
Use queryParamsHandling: 'merge' to update one param while keeping the rest:
this.router.navigate([], {
queryParams: { order: 'desc' },
queryParamsHandling: 'merge',
relativeTo: this.route,
});
Reading query params
Option A — input() signal (Angular 16+)
With withComponentInputBinding() enabled, query params are automatically bound to inputs whose name matches the param key:
@Component({ ... })
export class TaskListComponent {
order = input<'asc' | 'desc'>('asc'); // bound to ?order=...
}
<!-- read and pass the current value back when linking -->
<a routerLink="." [queryParams]="{ order: order() }">Re-sort</a>
Option B — observable
@Component({ ... })
export class TaskListComponent {
private route = inject(ActivatedRoute);
order: 'asc' | 'desc' = 'asc';
constructor() {
this.route.queryParams
.pipe(takeUntilDestroyed())
.subscribe(params => {
this.order = params['order'] ?? 'asc';
});
}
}
10. Route Data Resolvers
A resolver fetches data before a route activates, so the component receives it immediately on load instead of having to handle a loading state itself. Define one with ResolveFn:
// user-tasks.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, ActivatedRouteSnapshot } from '@angular/router';
import { TaskService } from './task.service';
import { Task } from './task.model';
export const userTasksResolver: ResolveFn<Task[]> = (route: ActivatedRouteSnapshot) => {
const userId = route.paramMap.get('userId')!;
return inject(TaskService).getTasksByUser(userId);
};
// user-friends.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn, ActivatedRouteSnapshot } from '@angular/router';
import { UserService } from './user.service';
import { User } from './user.model';
export const userFriendsResolver: ResolveFn<User[]> = (route: ActivatedRouteSnapshot) => {
const userId = route.paramMap.get('userId')!;
return inject(UserService).getFriends(userId);
};
Attach both to the route under the resolve key:
{
path: 'users/:userId',
component: UserDetailComponent,
resolve: {
tasks: userTasksResolver,
friends: userFriendsResolver,
},
}
By default, resolvers only re-run when path params change. Use runGuardsAndResolvers to control when they execute:
{
path: 'users/:userId',
component: UserDetailComponent,
resolve: { tasks: userTasksResolver },
runGuardsAndResolvers: 'paramsOrQueryParamsChange', // also re-run on ?sort=... changes
}
| Value | Re-runs when |
|---|---|
paramsChange (default) |
Path or path params change |
pathParamsChange |
Only path params change (ignores query params) |
paramsOrQueryParamsChange |
Path params or query params change |
always |
Every navigation, regardless of what changed |
Read the resolved data in the component via ActivatedRoute.data:
import { Component, inject, computed } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({ ... })
export class UserDetailComponent {
private data = toSignal(inject(ActivatedRoute).data);
tasks = computed(() => this.data()?.['tasks']);
friends = computed(() => this.data()?.['friends']);
}
Or with withComponentInputBinding() (Angular 16+), resolved data is bound directly to inputs — no ActivatedRoute needed:
@Component({ ... })
export class UserDetailComponent {
tasks = input<Task[]>();
friends = input<User[]>();
}
The resolver can return a value, a
Promise, or anObservable. Angular waits for it to complete before rendering the component.
Comparison to React Router loaders
React Router v6.4+ has a very similar concept called loaders. The main differences:
| Angular resolver | React loader | |
|---|---|---|
| Definition | Separate ResolveFn function |
Inline loader function on the route object |
| Reading data |
ActivatedRoute.data or input()
|
useLoaderData() hook |
| Multiple resolvers | Multiple keys under resolve: {}
|
Single loader, return an object with multiple keys |
| Cancellation | Unsubscribes Observable on navigation cancel | Uses request.signal (AbortSignal) |
Both block navigation until data is ready, and both support returning Promises or observables/async values.
11. Route Guards
Guards are functions that run before (or after) a navigation and decide whether it should proceed. Since Angular 14, the preferred style is functional guards — plain functions using inject(), no class needed. Class-based guards still work but are deprecated.
| Guard | Applied on route as | Runs when |
|---|---|---|
canActivate |
canActivate: [guard] |
Entering a route |
canActivateChild |
canActivateChild: [guard] |
Entering any child route |
canDeactivate |
canDeactivate: [guard] |
Leaving a route |
canMatch |
canMatch: [guard] |
Deciding whether a route can be matched at all |
A guard returns true to allow navigation, false to block it, or a RedirectCommand / UrlTree to redirect instead.
canActivate — protect a route
// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, RedirectCommand } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const auth = inject(AuthService);
const router = inject(Router);
if (auth.isLoggedIn()) {
return true;
}
return new RedirectCommand(router.parseUrl('/login'));
};
Attach it to a route:
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [authGuard],
}
Return
new RedirectCommand(urlTree)instead offalse— it redirects the user cleanly without requiring a separaterouter.navigate()call.
canDeactivate — guard against leaving
Useful for unsaved form changes. The guard receives the current component instance, so it can check its state:
import { CanDeactivateFn } from '@angular/router';
import { TaskEditComponent } from './task-edit.component';
export const unsavedChangesGuard: CanDeactivateFn<TaskEditComponent> = (component) => {
if (component.hasUnsavedChanges()) {
return confirm('You have unsaved changes. Leave anyway?');
}
return true;
};
{
path: 'tasks/:id/edit',
component: TaskEditComponent,
canDeactivate: [unsavedChangesGuard],
}
canMatch — conditional route matching
Unlike canActivate (which blocks navigation), canMatch makes the route invisible to the router — if it returns false, Angular falls through to the next matching route. Useful for feature flags or role-based route variants:
import { CanMatchFn } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const adminOnlyGuard: CanMatchFn = () => {
return inject(AuthService).isAdmin();
};
{ path: 'settings', component: AdminSettingsComponent, canMatch: [adminOnlyGuard] },
{ path: 'settings', component: UserSettingsComponent }, // fallback for non-admins
Top comments (0)