Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do my angular2 components get re-instantiated each time I change the route?

I am trying to put together a demo app of Angular 2 + Rx.JS 5/Next.

I noticed that my components are re-instantiated each time I switch the route.

Here is the code for the root app:

import {bootstrap}    from 'angular2/platform/browser';
import {HTTP_PROVIDERS} from 'angular2/http';
import {ROUTER_PROVIDERS} from 'angular2/router';
import {AppComponent} from './app.component.ts';

bootstrap(AppComponent, [HTTP_PROVIDERS, ROUTER_PROVIDERS]);

Here is the code for the root component:

import {Component} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {FirstComponent} from './app.first-component.ts';
import {SecondComponent} from './app.second-component.ts';
import {AppService} from "./app.services.ts";


@Component({
    selector: 'my-app',
    providers: [AppService, FirstComponent, SecondComponent],
    directives: [FirstComponent, SecondComponent, ROUTER_DIRECTIVES],
    template: `<h1>An Angular 2 App</h1>
               <a [routerLink]="['First']">first-default</a> 
               <a [routerLink]="['Second']">second</a> 
               <router-outlet></router-outlet>`
})
@RouteConfig([
    {path: '/', name: 'First', component: FirstComponent, useAsDefault: true},
    {path: '/second', name: 'Second', component: SecondComponent}
])
export class AppComponent {
}

Then the code for the first component (mapped to /):

import {Component, OnInit, NgZone} from "angular2/core";
import {AppService} from "./app.services.ts";
import 'rxjs/Rx';


@Component({
    selector: 'my-first',
    template: `
<div>
    <ul>
        <li *ngFor="#s of someStrings">
           a string: {{ s }}
        </li>
    </ul>
 </div>`
})
export class FirstComponent implements OnInit {

    zone:NgZone;

    constructor(private appService:AppService) {
        console.log('constructor', 'first');
        this.zone = new NgZone({enableLongStackTrace: false});
    }

    someStrings:string[] = [];

    ngOnInit() {
        console.log('ngOnInit', 'first');
        this.appService.refCounted.subscribe(
            theStrings=> {
                this.zone.run(() =>this.someStrings.push(...theStrings));
            },
            error=>console.log(error)
        );
    }
}

And the second component (mapped to /second):

import {Component, OnInit, NgZone} from "angular2/core";
import {AppService} from "./app.services.ts";

@Component({
    selector: 'my-second',
    template: `
<div>
    <ul>
        <li *ngFor="#s of someStrings">
           a string: {{ s }}
        </li>
    </ul>
 </div>`
})
export class SecondComponent implements OnInit {

    zone:NgZone;

    constructor(private appService:AppService) {
        console.log('constructor', 'second');
        this.zone = new NgZone({enableLongStackTrace: false});
    }

    someStrings:string[] = [];

    ngOnInit() {
        console.log('ngOnInit', 'second');
        this.appService.refCounted.subscribe(
            theStrings=> {
                this.zone.run(() =>this.someStrings.push(...theStrings));
            },
            error=>console.log(error)
        );
    }
}

And finally the app service (slightly less relevant to this question):

import {Injectable} from "angular2/core";
import {Observable} from "rxjs/Observable";
import {Subject} from "rxjs/Subject";
import 'rxjs/Rx';


@Injectable()
export class AppService {

    constructor(){
        console.log('constructor', 'appService');
    }

    someObservable$:Observable<string[]> = Observable.create(observer => {
        const eventSource = new EventSource('/interval-sse-observable');
        eventSource.onmessage = x => observer.next(JSON.parse(x.data));
        eventSource.onerror = x => observer.error(console.log('EventSource failed'));

        return () => {
            eventSource.close();
        };
    });

    subject$ = new Subject();

    refCounted = this.someObservable$.multicast(this.subject$).refCount();

    someMethod_() {
        let someObservable$:Observable<string[]> = Observable.create(observer => {
            const eventSource = new EventSource('/interval-sse-observable');
            eventSource.onmessage = x => observer.next(JSON.parse(x.data));
            eventSource.onerror = x => observer.error(console.log('EventSource failed'));

            return () => {
                eventSource.close();
            };
        });
        return someObservable$;
    }
}

So in order to debug the instantiation of the First and Second components, I have added a console.log in the constructors/ngOnInit:

and I noticed that each time I change the route by clicking on the links, I get:

constructor first
ngOnInit first

constructor second
ngOnInit second
...

Can someone please advise whether this is expected behavior? If so how can I get Angular2 to instantiate my components only once?

Note that I have explicitly required that the First and Second components be instantiated at the root-level component by adding a providers array there.

P.S. Here is the github repository for this project: https://github.com/balteo/demo-angular2-rxjs/tree/WITH-ROUTER

edit:

I am still trying to find a solution to this problem. I am getting something wrong with the router or my components. I have pushed the app to github here hoping someone can provide advice.

like image 481
balteo Avatar asked Apr 26 '16 10:04

balteo


People also ask

Is ngOnDestroy called on route change?

ngOnDestroy doesn't get called because some components do not get destroyed when navigating to a different route.

What is the correct way to add a basic route in Angular?

First, add links to the two components. Assign the anchor tag that you want to add the route to the routerLink attribute. Set the value of the attribute to the component to show when a user clicks on each link. Next, update your component template to include <router-outlet> .

Would you like to add Angular routing meaning?

Angular routing is the method to direct the users to the relevant page that they want to perform their actions. In other words, There may be multiple components in a single angular app. Routing is the way to implement the connection between those components. If you say yes Angular will add app-routing.


1 Answers

>= 2.3.0-rc.0

A custom RouteReuseStrategy can be implemented to control when routed components are destroyed and recreated or reused.

  • https://angular.io/docs/ts/latest/api/router/index/RouteReuseStrategy-class.html
  • https://www.softwarearchitekt.at/post/2016/12/02/sticky-routes-in-angular-2-3-with-routereusestrategy.aspx
  • https://github.com/angular/angular/issues/7757

>= 2.0.0

CanReuse doesn't exist anymore in the new router. When only route parameters change while staying on the same route, the component is not recreated.

If the route is changed and navigated back to the same component the component is recreated.

<=RC.x

Note that I have explicitly required that the First and Second components be instantiated at the root-level component by adding a providers array there.

Adding components to providers: [] is mostly meaningless especially in regard to this issue.

You can implement CanReuse

by adding

routerCanReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; }

to your component, but it's quite limited for which kind of navigation the same instance is reused (if you stay on the same route and only change parameters).

If CanReuse doesn't fix your problem then move the data to a service, where instances are kept and bind the view of the component to the data of this service to make the component show current data.

like image 164
Günter Zöchbauer Avatar answered Sep 29 '22 11:09

Günter Zöchbauer