What's New in Angular 7 Navigation

PR #25740 consolidates all router-driven navigations into a single observable stream. Additionally, the router now permits only one active navigation at a given moment. These adjustments make navigation both faster and more deterministic. While largely internal, they reshape how we approach routing logic in our apps.

This is a significant overhaul of the router's previous architecture. The refactor brings several key benefits, and future enhancements will build upon this foundation.

This functionality shipped with Angular 7.0. In this piece, we'll dig into what changed and how to leverage it, including how switchMap guarantees a single in-flight navigation.

How Navigation Works

Any URL change triggers a navigation, whether it's initiated imperatively (like a service invoking navigate or a guard returning a UrlTree) or through user interaction with a [routerLink] directive. Once kicked off, a navigation passes through these phases:

  • Handling redirects
  • Matching the URL to routes
  • Executing guards and resolvers
  • Rendering components and syncing the browser URL

A deeper dive into each stage is available in my earlier write-up.

The Issue at Hand

Prior to #25740, concurrent navigations were possible, which led to tricky scenarios. Take this example:

  • A user clicks link X, starting navigation 1.
  • Navigation 1's guards and resolvers run asynchronously, taking 10 seconds.
  • Meanwhile, the user clicks link Y, launching navigation 2.
  • Navigation 2 stalls until navigation 1's async work finishes (though those results get discarded).
  • If a guard in navigation 1 triggers a redirect while navigation 2 waits, the final destination becomes unpredictable.

Jason Aden covered this very issue in his AngularConnect 2018 talk, which is worth a watch.

Handling overlapping navigations was cumbersome, both internally and for developers. Thanks to #25740, only one navigation can be active at any time.

As we'll see, this makes the whole process much simpler to follow.

What Changed

The core internal updates from #25740 include:

  • Each step in the navigation lifecycle now maps to its own dedicated operator (e.g., redirects, route recognition).
  • These operators feed into a switchMap, so only the latest navigation is processed, with any ongoing ones automatically cancelled and tidied up.

Diving Deeper

A single operator with big impact
The "one navigation at a time" rule is upheld by the switchMap operator. By routing all navigations through a unified observable stream piped with switchMap—instead of the previous mergeMap—any incoming navigation causes the prior one to be cancelled and cleaned up automatically.

Refactor switchMap instead of the previous mergeMap to ensure new navigations cause a cancellation and clean up of already running navigations
– excerpt from PR 25740

If you're unfamiliar with how switchMap shines, Nicholas Jamieson's article on the topic is a great starting point.

Custom operators in the pipeline

With #25740, the navigation process is broken into custom operators located under /packages/router/src/ operators. Each stage from earlier is now its own operator:

  • apply_redirects.ts
  • recognize.ts
  • check_guards.ts and resolve_data.ts
  • activate_routes.ts

Internally, each navigation is represented by the NavigationTransition type. The router.transitions observable—which emits these transitions—works alongside the main router.navigations observable to manage new navigations. Here, switchMap ensures that any in-progress navigation gets cancelled the moment a new one starts.

private setupNavigations(transitions: Observable<NavigationTransition>): Observable<NavigationTransition> {
    return transitions.pipe(
        filter(t => t.id !== 0),
        // Extract URL
        map(t => ({...t, extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl)}) as NavigationTransition),
         // Using switchMap so we cancel executing navigations when a new one comes in
        switchMap(t => {

You can review the full pipeline in setupNavigations within router.ts.

Why It Matters

With these updates, starting a fresh navigation immediately cancels any pending ones. That translates to fewer memory leaks and no wasted cycles on guards or resolvers for obsolete navigations. For larger apps, the impact is especially noticeable.

The good news is all of this is internal. There's nothing you need to modify in your code. Just keep in mind that only one navigation runs at a time.

Wrapping Up

To recap the impact of #25740:

  • Only a single navigation can be active at any moment.
  • Triggering a new navigation cancels and cleans up any pending one.
  • Internally, each navigation stage is now a separate, composable operator.
  • Expect faster and more predictable navigation behavior.

Happy navigating!