Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Mallory Woods
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Mallory Woods@THEMalloryWoods )

View Components May Get Unnecessarily Reinstantiated Under Certain Circumstances In Angular 4.4.6

By Ben Nadel on

UPDATE 2017-10-27: This behavior has nothing to do with the ActivatedRoute. If you remove the ActivatedRoute from this demo and just perform a router.navigate() call directly from within the ngOnInit() life-cycle method, the same behavior is exhibited. As such, the race-condition is somewhere between the Component rendering and the navigation event.

UPDATE 2017-10-28: Ok wait, the more I noodle on this, the stranger it gets. At first, I thought maybe it was navigating away from the component too soon after it was created; but now, I'm realizing that this is also symptomatic if I navigate within the ActivatedRoute ParamMap subscription much longer after the component has been rendered - see updated code at the bottom.

---

As I've been doing a deep-dive into the Router in Angular 4.4.6, I've run across a strange behavior. There appears to be - as best I can tell - some sort of race condition that occurs if a View component tries to navigate away from itself too quickly. In such a circumstance, the View component will be destroyed and then immediately recreated. And, to make the circumstances even more mysterious, the issue only becomes symptomatic if you manually edit the browser URL; or, if you click on a vanilla HREF link. If you use a routerLink, things work as expected.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

To demonstrate this funky behavior, all we have to do is create a View component that binds to its own ActivatedRoute's ParamMap. Then, when the ParamMap callback is invoked, we navigate away from the View component:

  • // Import the core angular services.
  • import { ActivatedRoute } from "@angular/router";
  • import { Component } from "@angular/core";
  • import { ParamMap } from "@angular/router";
  • import { Router } from "@angular/router";
  • import { Subscription } from "rxjs/Subscription";
  •  
  • // Import these modules for their side-effects.
  • import "rxjs/add/operator/delay";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "child-component",
  • styleUrls: [ "./child.component.less" ],
  • template:
  • `
  • This is the <strong>Child</strong> component.
  • `
  • })
  • export class ChildComponent {
  •  
  • private activatedRoute: ActivatedRoute;
  • private paramMapSubscription: Subscription;
  • private router: Router;
  •  
  • // I initialize the child-view component.
  • constructor(
  • activatedRoute: ActivatedRoute,
  • router: Router
  • ) {
  •  
  • this.activatedRoute = activatedRoute;
  • this.router = router;
  •  
  • console.group( "Child Component Instantiated." );
  •  
  • }
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  • // I get called once when the component is being destroyed.
  • public ngOnDestroy() : void {
  •  
  • ( this.paramMapSubscription ) && this.paramMapSubscription.unsubscribe();
  •  
  • console.log( "ngOnDestroy() called." );
  • console.groupEnd();
  •  
  • }
  •  
  •  
  • // I get called once, after the inputs have been bound for the first time.
  • public ngOnInit() : void {
  •  
  • this.activatedRoute.paramMap
  • // CAUTION: Without the .delay(0) here, the component will be destroyed and
  • // immediately re-created under certain circumstances (such as when hand
  • // crafting a bad URL or by clicking on a bad HREF). As best I can GUESS,
  • // there's some sort of RACE CONDITION when the navigation is performed too
  • // close (timing-wise) to the execution of the ParamMap callback. To be
  • // honest, I am not sure I fully understand what is going on....
  • // --
  • // .delay(0)
  • // --
  • .subscribe(
  • ( paramMap: ParamMap ) : void => {
  •  
  • console.warn( "Param-map value:", paramMap.get( "id" ) );
  • this.router.navigate( [ "/" ] );
  •  
  • }
  • )
  • ;
  •  
  • }
  •  
  • }

As you can see, inside the paramMap.subscribe() callback, we're immediately navigating back to the root of the application. You may have noticed that I have a commented-out .delay(0) statement. For some reason, this goes away if I put a clock-tick between the paramMap stream event and the navigation - which is why I believe this is some sort of race condition.

Now, in the root component, we're providing a number of different types of entry-points to the Child View component:

  • RouterLink property.
  • RouterLink attribute interpolation.
  • Vanilla HREF.

Before you write off the HREF as an invalid use-case, remember that this is akin to hand-editing the URL; or, copy-pasting the URL into an already-rendered application.

  • // Import the core angular services.
  • import { Component } from "@angular/core";
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • @Component({
  • selector: "my-app",
  • styleUrls: [ "./app.component.less" ],
  • template:
  • `
  • <ul>
  • <li>
  • <a [routerLink]="[ 'child', 1 ]">Show Child 1</a> &mdash; (Router Link Property)
  • </li>
  • <li>
  • <a routerLink="./child/2">Show Child 2</a> &mdash; (Router Link Attribute)
  • </li>
  • <li>
  • <a href="#/child/3">Show Child 3</a> &mdash; (Vanilla HREF)
  •  
  • &mdash;
  • <strong>This one has FUNKY behavior</strong>.
  •  
  • Now, before you go and write this off as an invalid use-case, remember
  • that this is the same as if I copy/paste a URL into the browser; or, if I
  • manually manipuate the URL.
  • </li>
  • </ul>
  •  
  • <router-outlet></router-outlet>
  • `
  • })
  • export class AppComponent {
  • // ...
  • }

Now, if we run this application in the browser, we can see that the first two RouterLink examples execute just as we would expect - the Child Component is instantiated and then immediately destroyed as the code programmatically navigates away from it:


 
 
 

 
 Possible race-condition when navigating away from a view component in Angular 4.4.6. 
 
 
 

If, however, we do any of the following:

  • Hit the browser back-button to return to the routerLink-based URL.
  • Click on the HREF example.
  • Copy-and-paste one of the routerLink URLs into the browser URL.

... then we get the following output:


 
 
 

 
 Possible race-condition when navigating away from a view component in Angular 4.4.6. 
 
 
 

WAT?! Obviously, this is unexpected behavior (at least as far as I can tell from the documentation). Through some trial-and-error, I discovered that you can work-around this by adding a .delay(0) to the ParamMap stream (see the video). This gives the underlying race-condition time to fix itself before the navigation event takes place.

Happy Friday!

UPDATE 2017-10-28

Ok, so in the code above, I'm navigating away from the component right after it gets created. Which lead me to think this behavior related to an initial-rendering life-cycle race-condition. However, I'm now realizing that it's even more complicated than that. I updated the Child Component to only navigate back to the root if the incoming ParamMap id value is other than "1":

  • // I get called once, after the inputs have been bound for the first time.
  • public ngOnInit() : void {
  •  
  • console.log( "ngOnInit() called." );
  •  
  • this.activatedRoute.paramMap
  • // CAUTION: Without the .delay(0) here, the component will be destroyed and
  • // immediately re-created under certain circumstances (such as when hand
  • // crafting a bad URL or by clicking on a bad HREF). As best I can GUESS,
  • // there's some sort of RACE CONDITION when the navigation is performed too
  • // close (timing-wise) to the execution of the ParamMap callback. To be
  • // honest, I am not sure I fully understand what is going on....
  • // --
  • // .delay(0)
  • // --
  • .subscribe(
  • ( paramMap: ParamMap ) : void => {
  •  
  • console.warn( "Param-map value:", paramMap.get( "id" ) );
  •  
  • // Only navigate away from the component if the route parameter is
  • // not "1". This will allow us to navigate to the "1" parameters for
  • // SEVERAL SECONDS before switching to "2". And, even though the
  • // component has been rendered for several seconds, we still see the
  • // strange behavior when navigating away using HREF or manually-
  • // edited URLs.
  • if ( paramMap.get( "id" ) !== "1" ) {
  •  
  • this.router.navigate( [ "/" ] );
  •  
  • }
  •  
  • }
  • )
  • ;
  •  
  • }

Now, we can test two behaviors:

  • Navigating away from a component immediately after initialization.
  • Navigating away from a component long after initialization, but right after a paramMap callback.

We already tested the first item. So now, if I open the app in the browser, navigate to the "1" parameter, let it sit for a few minutes, then navigate to the "3" parameter, we see the following output:


 
 
 

 
 There seems to be some sort of race-condition in the ActivatedRoute paramMap callback. 
 
 
 

As you can see, the Child Component is getting destroyed, re-created, and then destroyed again, despite the fact that we navigate to the "3" route param several seconds after the Child Component has been fully initialized and mounted on the DOM (Document Object Model).

So, the issue is more complicated than I had originally thought. I'm now thinking it has something to do with the ActivatedRoute. But, still, I cannot put my finger on it.



Looking For A New Job?

Ooops, there are no jobs. Post one now for only $29 and own this real estate!

100% of job board revenue is donated to Kiva. Loans that change livesFind out more »

Reader Comments

@All,

Right after I published this, it occurred to me to try and do this without the ActivatedRoute ParamMap ... and, of course, the same behavior occurs. So, clearly this has nothing to do with the ParamMap subscription; the race-condition is more closely tied to the view rendering and the navigation before ... something - maybe before the component has a chance to run through its view-life-cycle events? Just guessing - I have no real clue.

Reply to this Comment

@All,

Last night, I tossed-and-turned in bed because something wasn't sitting right. And, I realized that I had seen this behavior from within the ParamMap callback even long after the component has been initialized. As such, I don't think the race-condition is about the initialization timing of the component -- I am back to thinking the race condition is strongly tied to the Router or the ActivatedRoute. It's like the activated route doesn't like to let go of the component or something. I still can't articulate the problem. But, at least I can see demonstrate it clearly.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.