Skip to content

Debugging Angular Navigation: Why NavigationEnd Was Not Triggered

In a recent project, I encountered a subtle Angular routing issue where a page navigation completed successfully, but the NavigationEnd event listener I set up wasn't firing as expected. This led to a key question:

Why did NavigationEnd not trigger even though the route changed?

This post walks through the root cause, complete with a reproducible example, and ends with actionable advice to avoid this pitfall.

The Scenario: Route Changed, But No NavigationEnd

Reproducible Example

Suppose we have the following route setup:

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent
  },
  {
    path: 'users',
    loadChildren: () => import('./users/users.module').then(m => m.UsersModule)
  }
];

And in UsersModule, we have:

const routes: Routes = [
  {
    path: ':id/details',
    component: UserDetailsComponent
  }
];

Now imagine you are on a page like /dashboard, and you trigger a navigation to /users/123/details. Your UserDetailsComponent has logic like this:

constructor(private router: Router) {
  this.router.events.subscribe(event => {
    if (event instanceof NavigationEnd) {
      console.log('NavigationEnd triggered');
    }
  });
}

Expected: Console logs "NavigationEnd triggered".

Actual: Nothing happens!

Root Cause

The issue boils down to Angular's component lifecycle and timing:

NavigationEnd is only emitted after the new component is instantiated.

But in our case, the component subscribes to router events in its constructor, which is too late to catch the NavigationEnd event for the navigation that created it.

By the time the component is constructed and subscribes, NavigationEnd has already happened.

Visual Timeline

[Router starts navigation] → NavigationStart
                          → Guards & Resolvers
                          → NavigationEnd
                          → Component is created
                                         → constructor()
                                         → ngOnInit()

As you can see, placing the listener in the constructor or even ngOnInit() is too late to observe the NavigationEnd that caused the component to be loaded.

Solutions

✅ Solution 1: Global Router Listener

Move the event subscription to a parent component or even AppComponent, where the router is already alive.

// app.component.ts
constructor(private router: Router) {
  this.router.events.subscribe(event => {
    if (event instanceof NavigationEnd) {
      console.log('Global: NavigationEnd fired!');
    }
  });
}

This works reliably, because this component lives throughout the app lifecycle.

✅ Solution 2: Use ActivatedRoute.params Instead

If you just want to respond to ID changes:

ngOnInit(): void {
  this.route.params.subscribe(params => {
    this.loadUserDetails(params['id']);
  });
}

This avoids the need to listen to NavigationEnd altogether.

When You Do Need NavigationEnd

You still want to use NavigationEnd when:

  • You want to reset component state after every same-route navigation (e.g. /users/123/details to /users/456/details)
  • You depend on global side effects, such as page tracking, scroll restoration, or analytics

In those cases, global or parent-level subscription is preferred.

Conclusion

If your component isn’t catching NavigationEnd, it's likely due to subscribing after the event has already fired. Understanding Angular's lifecycle and the timing of router.events is key.

TL;DR: Subscribe early (ideally in a persistent component) if you want to observe NavigationEnd.

Comments