1

In Angular, after a routerLink triggers a route change, the old component is only destroyed basically at the same time as the new component's initialization: timeline of the Angular Router's default behaviour

This is being a problem for me because I want to show a spinner after the old component is destroyed and before the new component is built -- this is relevant when a component/module is lazy-loaded. Since the old component is only destroyed as late as the new component is being built, my current spinner is just laying over the old component while the new component is loaded.

So is there a way to config the router so it destroys the old component as soon as routerLink is triggered? Like this: timeline of the behaviour I desire for the Angular Router in my use case

I should also mention that I don't want to hide the router-outlet in any way because I'm also applying animations to the entering/leaving components, including the spinner.

Here's what that code is looking like, if it matters (it's app.component and I'm using Tailwind, btw):

@Component({
  styles: `
    :host ::ng-deep router-outlet + * { // target the routed components and the spinner
      &, & + * {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
      }
    }

    .router-outlet-container {
      position: relative;
      height: 100%;
      width: 100%;
    }

    .spinner-outer-container {
      position: relative;
      height: 100%;
      width: 100%;
    }

    .spinner-inner-container {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translateX(-50%) translateY(-50%);
    }
  `,
  animations: [
    trigger('routeAnimations', [
      transition('* <=> *', [
        query(':enter, :leave', [ style({
          position: 'absolute', left: 0, width: '100%', overflow: 'hidden',
        }) ], { optional: true }),
        group([
          query(':enter', [
            style({ opacity: '0' }),
            animate(animation, style({ opacity: '1' })),
          ], { optional: true }),
          query(':leave', [
            style({ opacity: '1' }),
            animate(animation, style({ opacity: '0' })),
          ], { optional: true }),
        ]),
      ]),
    ]),
  ],
})
export class MyComponent {
  #router = inject(Router);

  protected route = new Subject<string>();
  #lazyLoadingStarted$ = this.#router.events.pipe(
      filter(v => v instanceof RouteConfigLoadStart),
      map(() => '__lazy_loading__'),
  );
  protected routingState = toSignal(this.#lazyLoadingStarted$.pipe(mergeWith(this.route)));
}
<div [@routeAnimations]=routingState() class="router-outlet-container">
    <router-outlet #outlet="outlet" (activate)="route.next(outlet.activatedRoute.snapshot.url[0]?.path || '')"/>
    @if (routingState() === '__lazy_loading__') {
        <div class="spinner-outer-container">
            <div class="spinner-inner-container">
                <mat-spinner/>
            </div>
        </div>
    }
</div>

Update

my current spinner is just laying over the old component

I found out a little way to circumvent this. Basically turned the spinner's container into an overlay. Only had to change styles and the spinner's container:

:host {
  height: 100vh;
  width: 100vw;
  display: flex;
  flex-direction: column:

  .spinner-overlay, ::ng-deep router-outlet + * { // target the spinner and the routed components
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
  }

  .spinner-overlay {
    width: 100%;
    z-index: 10;
    background: radial-gradient(hsla(0, 0%, 100%, 0.79), hsla(0, 0%, 100%, 0.17));
  }
}
<div class="spinner-overlay">
    <div class="spinner-inner-container">
        <mat-spinner/>
    </div>
</div>

Meaning this question no longer causes problems for my use case, so now I'm only looking for an answer out of curiosity because there are probably other use cases where fine-grained customization of Router behaviour may be important.

3
  • The reason why both the old and new page component are rendered at the same time most likely has to do with supporting navigation animations. So I don't think the old component is supposed to be removed immediately anyway. I would expect that angular adds a class to the old component while the new one is being constructed Commented Oct 11, 2024 at 16:11
  • What angular version are you using? Commented Oct 15, 2024 at 7:35
  • Oh. You already have a spinner. Yeah, just give him width:100%;height:100%;background:white;opacity:0.8; and call it a day. Commented Oct 15, 2024 at 8:39

2 Answers 2

2
+50

Router may never destroy previous component, it can cache it for reuse. Previous component is not removed from DOM until router loads lazy module. You can wrap router outlet in custom component, subscribe to router events and show/hide outlet when needed e.g. apply visibility: hidden.

Sign up to request clarification or add additional context in comments.

3 Comments

This is the correct answer, though maybe show/hide a spinner rather than show/hiding the router-outlet. For example, a spinner with background:white; background-image:url(spinner.gif); width:100%;height:100%;position:absolute;left:0;top:0;z-index:2; opacity:0.7; can sit on top of your router-outlet, hiding it from view during router events.
so can't I ever control when the router removes the previous component from the DOM? can you give a reference?
I could not find any relevant options in router configuration, so I think you can't control this.
0

For me it sounds better to subscribe to Angular Router Events and build your spinner around that instead of the Lifecycle hooks.

For example, while NavigationStart you set somewhere a flag like showSpinner = true and at the NavigationEnd you set somewhere a flag like showSpinner = false. You could also build a service around that, that has some more logic about the current destructuring component and then the new one to maintain a robust solution.

Another alternative would be CanActivate und canDeactivate Route Guard to trigger a Subject, which sets the Spinner to true/false.

And last but not least another variant could be to write your own CustomRouteReuseStrategy and provide it globally in your app. With that you have full control over the router components lifecycle, but use it carefully, because it needs some core knowledge to do it properly.

1 Comment

in MyComponent I'm already using the RouteConfigLoadStart to know when the loading of a lazy-loaded route starts, and the (activate) event from the router outlet to know then the route changes

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.