BACK
Use casesRouter spinner — a global navigation loader
Use cases · 20

Router spinner — a global navigation loader

Pattern

Lazy routes, resolvers, and guards can take seconds to load. The user needs to see that the transition is being handled — a global spinner over the app shell.

The problem it solves

The "navigating" state can't be described by a single event — the Router produces a whole family: NavigationStart, NavigationEnd, NavigationCancel, NavigationError. Set loading=true on Start and forget Cancel/Error, and the spinner hangs forever after a guard cancels navigation.

Operators and why they matter

  • filter — keep only the four event types we need (Start + three terminal ones), ignoring noise like RoutesRecognized.
  • map — turn the event type into a boolean: NavigationStart → true, everything else → false.
  • startWith(false) — the app isn't loading initially.
  • distinctUntilChanged — two Starts in a row (on a redirect) don't flicker the spinner.

Gotchas

  • Forgetting Cancel/Error is the most common mistake. The spinner hangs at true forever after a guard cancels.
  • scan with a loader counter is only needed for parallel loads. Router navigation is sequential — a counter is redundant.
  • A spinner in each page component is a bad idea. Lazy loading happens BEFORE the component is created, so the spinner won't show in time. Only in the app shell.

What you get

One global stream covers the whole navigation lifecycle — start, success, cancel, and error — without manual flags in components.

script.ts // TypeScript
CONSOLE · Console output
Hit Run to see the result...