| |

Angular 78 🅰️ Router Internals and Events

The Angular Router is one of the framework’s most used features and one of its least understood. You configure routes, navigate with routerLink, and read parameters from ActivatedRoute. That is the API surface most developers interact with. But underneath, the router is a state machine that runs through a specific sequence of events for every navigation. It has a lifecycle, a set of guards, a state tree, and an event stream that you can observe.

The Router is not part of @angular/core. It is a separate package — @angular/router — that builds on top of the framework’s core APIs . It maintains a RouterState tree that mirrors the ActivatedRoute hierarchy, and it emits an Event object for every phase of navigation. Understanding those events is how you build loading indicators, diagnose redirect loops, and hook into navigation at the right moment.

Key point: Every Angular navigation emits a sequence of events: NavigationStart, RouteConfigLoadStart (for lazy routes), RoutesRecognized, GuardsCheckStart, ChildActivationStart, ActivationStart, GuardsCheckEnd, ResolveStart, ResolveEnd, ActivationEnd, ChildActivationEnd, NavigationEnd (success) or NavigationCancel / NavigationError (failure). Subscribing to router.events and filtering by event type is how you observe navigation without polling .


Why router internals matter

The router’s event stream is not a curiosity. It is the mechanism for several common requirements that have no other clean solution.

The loading indicator problem. A user clicks a link, and the target route’s component takes a second to load because it is lazy-loaded. Without a loading indicator, the user sees nothing happen. With a router event subscription, you can show a spinner on NavigationStart and hide it on NavigationEnd or NavigationCancel . This is the canonical use case for router.events.

The diagnostic problem. A redirect loop produces no error message. The page just keeps reloading. The router’s event stream shows the sequence — NavigationStart, RoutesRecognized, redirect, NavigationStart again — which makes the loop visible. Without events, diagnosing a redirect loop is guesswork.

The guard problem. Guards are functions or classes that return true, false, UrlTree, or an observable of these . They run during the GuardsCheckStart to GuardsCheckEnd window. Understanding the event sequence tells you when guards execute relative to resolve and activation.

The lazy-loading problem. Lazy routes are loaded during navigation. The RouteConfigLoadStart and RouteConfigLoadEnd events fire around the network request . If you want to show a specific loading state for lazy-loaded routes, these are the events to observe.

The trade-off. The event stream is verbose. Every navigation emits a dozen events, and most of them are not relevant to any given subscriber. The pattern is to filter by event instanceof NavigationStart or event instanceof NavigationEnd rather than handling every event. Angular’s own RouterOutlet uses this pattern internally.


a. The Router Event Sequence

Every successful navigation emits events in a fixed order. Understanding the order is the key to knowing when to do what.

The sequence for a navigation to a route without lazy loading or guards:

  1. NavigationStart — fired when navigation begins. Contains the target URL and navigation ID.
  2. RoutesRecognized — fired when the router has matched the URL to routes. Contains the target RouterStateSnapshot.
  3. GuardsCheckStart — fired when guard checking begins.
  4. ChildActivationStart — fired for each route being activated.
  5. ActivationStart — fired for each route component being activated.
  6. GuardsCheckEnd — fired when guard checking completes.
  7. ResolveStart — fired when resolvers begin fetching data.
  8. ResolveEnd — fired when resolvers finish.
  9. ActivationEnd — fired for each activated route.
  10. ChildActivationEnd — fired for each activated child route.
  11. NavigationEnd — fired when navigation completes successfully.

For lazy-loaded routes, RouteConfigLoadStart and RouteConfigLoadEnd fire between NavigationStart and RoutesRecognized .

For failed navigation, NavigationCancel fires instead of NavigationEnd when a guard returns false or a redirect occurs. NavigationError fires when a navigation throws an unhandled error .

The NavigationStart and NavigationEnd events are the two most commonly used. A loading indicator subscribes to the former to show the spinner and the latter to hide it.


b. The Navigation Lifecycle and Guards

Guards are functions that run during navigation and decide whether it should proceed. There are four types: CanActivate (can this route be activated?), CanActivateChild (can child routes be activated?), CanDeactivate (can the current route be deactivated?), and CanMatch (should the route be considered for matching at all?) .

Each guard returns one of four things: true (allow), false (block), a UrlTree (redirect), or an Observable or Promise of these . The router awaits the observable or promise before continuing.

Guards run between GuardsCheckStart and GuardsCheckEnd. If a guard returns false or a UrlTree, the navigation is cancelled and NavigationCancel fires . This is why the event sequence for a blocked navigation looks different from a successful one. The ResolveStart, ResolveEnd, and NavigationEnd events do not fire — the router never gets past the guard check.

The CanMatch guard is newer (Angular 15) and runs earlier than CanActivate. It determines whether a route configuration matches at all, allowing multiple routes with the same path and different guards. This is how route-based feature flags and A/B testing are implemented .

Resolvers run between ResolveStart and ResolveEnd. They pre-fetch data before the component activates, so the component receives the data via ActivatedRoute.data without a loading state . This is the mechanism for “resolve data before showing the page.”


c. The RouterState Tree and ActivatedRoute

The router maintains a tree of ActivatedRoute objects. Each ActivatedRoute represents a route in the hierarchy and exposes its parameters, data, and child routes.

The ActivatedRoute has two snapshots: the snapshot, which is the state at the moment the component activates, and the observables, which emit new values when the route changes . The observables are the correct choice for components that may be reused across route changes — a /users/1 to /users/2 navigation reuses the component, and the snapshot would be stale.

The ActivatedRoute observables are:

  • params — the route parameters (e.g., :id)
  • queryParams — the query string parameters
  • fragment — the URL fragment
  • data — the resolved data from resolvers
  • url — the URL segments
  • paramMap — a ParamMap with get, has, getAll
  • queryParamMap — same for query parameters

The router walks the route tree during navigation. Each level is activated in order — parent, then child, then grandchild. The ActivatedRoute tree mirrors this hierarchy. A component deep in the tree accesses its parent with route.parent, its children with route.children, and the root with route.root .


Complete Example Session

This session builds a loading indicator, a navigation logger, a route-level guard with redirect, and a component that reads the router state tree.

// ============================================
// PART 1: THE ROUTER EVENT SUBSCRIPTION
// ============================================

import { Component, inject, signal } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';
import { filter } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    @if (isLoading()) {
      <div class="loading-bar">Loading...</div>
    }
    <router-outlet />
  `,
})
export class AppComponent {
  private router = inject(Router);
  isLoading = signal(false);

  constructor() {
    this.router.events.pipe(
      filter(event =>
        event instanceof NavigationStart ||
        event instanceof NavigationEnd ||
        event instanceof NavigationCancel ||
        event instanceof NavigationError
      )
    ).subscribe(event => {
      if (event instanceof NavigationStart) {
        this.isLoading.set(true);
      } else {
        this.isLoading.set(false);
      }
    });
  }
}

// ============================================
// PART 2: THE NAVIGATION LOGGER
// ============================================

import { NavigationStart, RoutesRecognized, GuardsCheckStart, GuardsCheckEnd, ResolveStart, ResolveEnd, NavigationEnd, NavigationCancel, NavigationError } from '@angular/router';

@Component({
  selector: 'app-debug',
  template: '<p>Debug mode</p>',
})
export class DebugComponent {
  private router = inject(Router);

  constructor() {
    this.router.events.subscribe(event => {
      if (event instanceof NavigationStart) {
        console.log('NavigationStart:', event.url);
      } else if (event instanceof RoutesRecognized) {
        console.log('RoutesRecognized:', event.state.url);
      } else if (event instanceof GuardsCheckStart) {
        console.log('GuardsCheckStart');
      } else if (event instanceof GuardsCheckEnd) {
        console.log('GuardsCheckEnd:', event.shouldActivate);
      } else if (event instanceof ResolveStart) {
        console.log('ResolveStart');
      } else if (event instanceof ResolveEnd) {
        console.log('ResolveEnd');
      } else if (event instanceof NavigationEnd) {
        console.log('NavigationEnd:', event.urlAfterRedirects);
      } else if (event instanceof NavigationCancel) {
        console.log('NavigationCancel:', event.reason);
      } else if (event instanceof NavigationError) {
        console.log('NavigationError:', event.error);
      }
    });
  }
}

// ============================================
// PART 3: THE AUTH GUARD
// ============================================

import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  }

  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url },
  });
};

// Route configuration:
// { path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }

// ============================================
// PART 4: THE DATA RESOLVER
// ============================================

import { ResolveFn } from '@angular/router';
import { inject } from '@angular/core';
import { UserService, User } from './user.service';

export const userResolver: ResolveFn<User> = (route) => {
  const userService = inject(UserService);
  const id = route.paramMap.get('id')!;
  return userService.getUser(id);
};

// Route configuration:
// { path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } }

// ============================================
// PART 5: THE COMPONENT USING RESOLVED DATA
// ============================================

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()?.name }}</h1>',
})
export class UserDetailComponent {
  private route = inject(ActivatedRoute);
  user = toSignal(this.route.data.pipe(map(data => data['user'])));
}

// ============================================
// PART 6: THE CAN DEACTIVATE GUARD
// ============================================

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

export interface CanComponentDeactivate {
  canDeactivate: () => boolean;
}

export const unsavedChangesGuard: CanDeactivateFn<CanComponentDeactivate> = (component) => {
  return component.canDeactivate() || confirm('You have unsaved changes. Leave?');
};

// ============================================
// PART 7: THE CAN MATCH GUARD
// ============================================

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

export const featureFlagGuard: CanMatchFn = () => {
  const config = inject(FeatureConfigService);
  return config.isEnabled('newDashboard');
};

// Route configuration:
// { path: 'dashboard', component: NewDashboardComponent, canMatch: [featureFlagGuard] }
// { path: 'dashboard', component: OldDashboardComponent }

// ============================================
// PART 8: THE ACTIVATED ROUTE TREE
// ============================================

@Component({
  selector: 'app-route-info',
  template: `
    <p>Full URL: {{ route.snapshot.url.join('/') }}</p>
    <p>Parent: {{ route.parent?.snapshot.url.join('/') }}</p>
    <p>Children: {{ route.children.length }}</p>
    <p>Root: {{ route.root.snapshot.url.join('/') }}</p>
  `,
})
export class RouteInfoComponent {
  route = inject(ActivatedRoute);
}

// ============================================
// PART 9: THE LAZY ROUTE LOADING EVENTS
// ============================================

import { RouteConfigLoadStart, RouteConfigLoadEnd } from '@angular/router';

this.router.events.pipe(
  filter(event =>
    event instanceof RouteConfigLoadStart ||
    event instanceof RouteConfigLoadEnd
  )
).subscribe(event => {
  if (event instanceof RouteConfigLoadStart) {
    console.log('Loading lazy route:', event.route.path);
  } else {
    console.log('Lazy route loaded:', event.route.path);
  }
});

// ============================================
// PART 10: THE COMPLETE ROUTE CONFIGURATION
// ============================================

export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard],
  },
  {
    path: 'users/:id',
    component: UserDetailComponent,
    resolve: { user: userResolver },
    canDeactivate: [unsavedChangesGuard],
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
    canMatch: [adminGuard],
  },
  { path: '**', component: NotFoundComponent },
];

The ten parts cover the router event subscription, the navigation logger, the auth guard, the data resolver, the component using resolved data, the can-deactivate guard, the can-match guard, the activated route tree, lazy route loading events, and the complete route configuration.


Quick Reference

The Router Event Classes

EventWhen FiredContains
NavigationStartNavigation beginsurl, id
RouteConfigLoadStartLazy route loading beginsroute
RouteConfigLoadEndLazy route loading endsroute
RoutesRecognizedRoute matching donestate, urlAfterRedirects
GuardsCheckStartGuard checking beginsstate
GuardsCheckEndGuard checking endsshouldActivate
ResolveStartResolvers beginstate
ResolveEndResolvers finishstate
ActivationStartComponent activationsnapshot
ActivationEndComponent activation donesnapshot
ChildActivationStartChild routes activatingsnapshot
ChildActivationEndChild routes donesnapshot
NavigationEndNavigation succeedsurlAfterRedirects
NavigationCancelNavigation blockedreason
NavigationErrorNavigation failederror

The Guard Types

GuardRuns WhenCan Return
CanActivateBefore route activationtrue, false, UrlTree, Observable
CanActivateChildBefore child activationSame
CanDeactivateBefore leaving routeSame
CanMatchBefore route matchingSame

The ActivatedRoute Observables

ObservableContains
paramsRoute parameters as object
paramMapRoute parameters as ParamMap
queryParamsQuery string as object
queryParamMapQuery string as ParamMap
fragmentURL fragment
dataResolved data
urlURL segments

The Navigation Service Methods

MethodPurpose
router.navigate(url)Navigate to URL array
router.navigateByUrl(url)Navigate to URL string
router.createUrlTree(url)Create a redirect tree
router.eventsObservable of all events

Best Practices

✅ Do This:

// Filter router events by type
this.router.events.pipe(
  filter(event => event instanceof NavigationStart)
).subscribe(...)                                              // ✅
// Return a UrlTree from guards for redirects
return router.createUrlTree(['/login']);                     // ✅
// Use paramMap instead of params for type safety
route.paramMap.get('id')                                      // ✅
// Use resolvers to pre-fetch data
resolve: { user: userResolver }                              // ✅
// Unsubscribe from router event subscriptions
private destroy = inject(DestroyRef);
this.router.events.pipe(takeUntilDestroyed(this.destroy)).subscribe(...) // ✅

❌ Don’t Do This:

// Don't subscribe without filtering
this.router.events.subscribe(event => console.log(event));   // ❌ noisy
// Don't use snapshot if the component can be reused
route.snapshot.paramMap.get('id')                            // ❌ stale on param change
// Don't forget to unsubscribe
this.router.events.subscribe(...) // without takeUntilDestroyed  // ❌ leak
// Don't return false from a guard without a redirect
return false; // user sees nothing happen                     // ❌ poor UX

Common Pitfalls

PitfallWhy It HappensFix
Redirect loopGuard always redirectsCheck the target URL
Stale paramsUsing snapshot after param changeUse paramMap observable
Loading indicator stuckNot handling NavigationCancelHandle all terminal events
Resolver data missingResolver returned undefinedCheck resolver return type
Guard not runningWrong guard typeMatch guard type to route position
Memory leakRouter subscription not cleaned upUse takeUntilDestroyed

Real-World Examples

1. Loading Indicator

this.router.events.pipe(
  filter(e => e instanceof NavigationStart || e instanceof NavigationEnd)
).subscribe(e => this.isLoading.set(e instanceof NavigationStart));

2. Auth Guard

export const authGuard: CanActivateFn = () => {
  return inject(AuthService).isLoggedIn() || inject(Router).createUrlTree(['/login']);
};

3. Data Resolver

export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUser(route.paramMap.get('id')!);
};

4. Can Deactivate

export const unsavedGuard: CanDeactivateFn<FormComponent> = (c) => {
  return c.canDeactivate() || confirm('Discard changes?');
};

5. Can Match

export const flagGuard: CanMatchFn = () => inject(Config).isEnabled('feature');

6. Param Map

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

7. Route Data

this.route.data.subscribe(data => {
  this.user = data['user'];
});

8. Query Params

this.route.queryParamMap.subscribe(params => {
  this.search = params.get('q');
});

9. Route Tree Access

const root = this.route.root;
const children = this.route.children;

10. Lazy Loading Event

this.router.events.pipe(
  filter(e => e instanceof RouteConfigLoadStart)
).subscribe(e => console.log('Loading', e.route.path));

Visual

The Navigation Event Sequence

┌──────────────────────────────────────────────┐
│  SUCCESSFUL NAVIGATION                       │
│                                              │
│  NavigationStart                             │
│       │                                      │
│       ▼                                      │
│  RouteConfigLoadStart  ← lazy routes only    │
│  RouteConfigLoadEnd                          │
│       │                                      │
│       ▼                                      │
│  RoutesRecognized                            │
│       │                                      │
│       ▼                                      │
│  GuardsCheckStart                            │
│  ChildActivationStart                        │
│  ActivationStart                             │
│  GuardsCheckEnd                              │
│       │                                      │
│       ▼                                      │
│  ResolveStart                                │
│  ResolveEnd                                  │
│       │                                      │
│       ▼                                      │
│  ActivationEnd                               │
│  ChildActivationEnd                          │
│       │                                      │
│       ▼                                      │
│  NavigationEnd                               │
│                                              │
└──────────────────────────────────────────────┘

The Guard Decision Flow

┌──────────────────────────────────────────────┐
│  GUARD DECISION                              │
│                                              │
│  CanActivate / CanMatch fires                │
│       │                                      │
│       ├─ returns true → continue             │
│       │                                      │
│       ├─ returns false → NavigationCancel    │
│       │                                      │
│       ├─ returns UrlTree → redirect          │
│       │   └─ new NavigationStart             │
│       │                                      │
│       └─ returns Observable → await          │
│           └─ then same decisions             │
│                                              │
└──────────────────────────────────────────────┘

The ActivatedRoute Tree

┌──────────────────────────────────────────────┐
│  ACTIVATED ROUTE TREE                        │
│                                              │
│  Root                                        │
│   └─ /users (parent)                         │
│       └─ /users/:id (child)                  │
│           └─ /users/:id/edit (grandchild)    │
│                                              │
│  Each node has:                              │
│    ├─ snapshot (state at activation)         │
│    ├─ params (observable)                    │
│    ├─ data (observable)                      │
│    ├─ parent (up the tree)                   │
│    └─ children (down the tree)               │
│                                              │
└──────────────────────────────────────────────┘

Terminal Events

┌──────────────────────────────────────────────┐
│  TERMINAL EVENTS                             │
│                                              │
│  NavigationEnd                               │
│    └─ Success. Component activated.          │
│                                              │
│  NavigationCancel                            │
│    └─ Guard returned false.                  │
│    └─ Redirect UrlTree returned.             │
│                                              │
│  NavigationError                             │
│    └─ Unhandled error during navigation.     │
│                                              │
│  Every navigation ends with exactly one      │
│  of these three.                             │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Router package@angular/router
Event streamrouter.events (Observable)
First eventNavigationStart
Last event (success)NavigationEnd
Last event (cancel)NavigationCancel
Last event (error)NavigationError
Lazy load eventsRouteConfigLoadStart, RouteConfigLoadEnd
Guard eventsGuardsCheckStart, GuardsCheckEnd
Resolver eventsResolveStart, ResolveEnd
Guard typesCanActivate, CanActivateChild, CanDeactivate, CanMatch
Route treeActivatedRoute with parent, children, root
Observablesparams, queryParams, data, fragment, url
MapsparamMap, queryParamMap

Key takeaways:

  • Every navigation emits a fixed sequence of events. NavigationStart begins, NavigationEnd (or NavigationCancel or NavigationError) ends. Between them are guard checks, resolver execution, and route activation. Filtering by event type is the pattern for observing navigation .
  • NavigationStart and NavigationEnd are the most-used events. A loading indicator subscribes to both: show on start, hide on end. Handle NavigationCancel and NavigationError as terminal events so the indicator never gets stuck .
  • Guards run between GuardsCheckStart and GuardsCheckEnd. They return true, false, a UrlTree, or an observable of these. A false cancels the navigation; a UrlTree redirects .
  • CanMatch runs earlier than CanActivate. It determines whether a route configuration matches at all, allowing multiple configurations for the same path with different guards. This is how feature flags and A/B testing are implemented .
  • Resolvers run between ResolveStart and ResolveEnd. They pre-fetch data so the component receives it via ActivatedRoute.data without a loading state .
  • The ActivatedRoute tree mirrors the route hierarchy. Each node has parent, children, and root references. Use these to navigate up and down the tree from within a component .
  • Use paramMap observables, not snapshots, for components that can be reused. A /users/1 to /users/2 navigation reuses the component. The snapshot is stale; the observable emits the new value .
  • Unsubscribe from router event subscriptions. Router events fire frequently. A subscription that is never cleaned up leaks memory. Use takeUntilDestroyed with an injection context.

Remember: The Angular Router is a state machine, and its event stream is the window into that machine. Every navigation runs through a fixed sequence: match, guard, resolve, activate. The events tell you which phase you are in and when it completes. Use them for loading indicators, diagnostics, and lazy-loading feedback. Use guards for access control and conditional navigation. Use resolvers to pre-fetch data. Use the ActivatedRoute observables to react to parameter changes. And always subscribe with a filter — the event stream is noisy, and most events are not relevant to any given subscriber. Understanding the sequence is the difference between reacting to navigation and fighting it.


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!