3

I have used the application in this GitHub repository as a demonstration app for my angular routing problem.

The application is working fine. Therefore, the application itself is not my focus as it is so simple, however you can access to its code completely if you want!

My problem is, that each time by navigation in this angular app, a new instance of the related component will be generated.

I made a few changes in the code to illustrate this instantiation. I added a simple counter as follow:

export class ContactListComponent implements OnInit {

  contacts: any[] = [];
  counter = 0; //<--- counter definition

  constructor(private contactService: ContactService) {
    console.log("List Component constructor: #" + ++this.counter); //<--- usage #1
  }

  ngOnInit() {
    console.log("List Component ngInit: #" + ++this.counter); //<--- usage #2
    this.contactService.getContacts().subscribe((data: any[]) => {
      this.contacts = data;
    });
  }
}

If you look at the following figure, by each navigation that I did a new instance has been generated, therefore the counter each time will be reset and in the console it shows the ngInit and constructor has been called again and again:

enter image description here

I even tried to navigate with the following snippet, and I got the same result!:

import { Router, ActivatedRoute } from '@angular/router';
constructor(
        private router: Router
    ) { }

this.router.navigate(['/contacts']);

The question is how can I prevent this new instantiation!?

Specially, when we navigate back from second page to the first page. I am looking for a technique like a singleton that will instantiate the component only the first time that we visit a route and the other times that we visit that route, we get the same instant of the component.

Actually this app is not the main app that I am working on, my main problem is in other app that I used a subscription technique for sharing data and then when I have several instances of a component, the following code in different instances will be fired and result not as expected. Why? Because if (this.agreeTerms) will have different value in different instances at the same time!

navNext(next) {
        this.next = next;
        if (this.next === true && this.router.url.startsWith('/') && this.router.url.endsWith('/')) {            
            this.data.unsetNext();
            if (this.agreeTerms) { //<-----
                this.router.navigate(['/form']);
            } else {
                this.error = 'Please accept the terms and conditions';
                this.scrollBottom();
            }
        }
    }
MJBZA
  • 4,796
  • 8
  • 48
  • 97
  • That's the way Angular works. It will create components afresh for router-outlet. Why don't you want the component to be re-created? If you were issue is component is loading data from backend and that can slow down component rendering, then, you should consider using services where data can be cached. – Wand Maker May 17 '20 at 19:30
  • No, actually the problem is it will keep the instances and will not destroy them. For example in other JS frameworks like UI5 it will not generate a new instance each time. I think the app component seems to be a parent component and by navigation each time an instance most be created or activated is exist! – MJBZA May 17 '20 at 19:31
  • @MahdiJ.Ansari - swimming against the tide is never a good idea. Since this is the way the router works in Angular I'd suggest considering other possibilities like (re-)designing your application in such a way that creation of the new component instance would not be a problem. – Alexander Leonov May 17 '20 at 19:39
  • @AlexanderLeonov You are absolutely right, but the question is why if the angular create a new instance does not destroy the previous instances of that component!? – MJBZA May 17 '20 at 19:45
  • yes this this how angular works. and the instances are not destroyed when you route away, which means if you have javascript running, it will continue to run after you route away. and if you route back, you potentially can have 2 instances of the javascript running simultaneously. so keep that in mind when writing your code. – Rick May 17 '20 at 19:45
  • @Rick and what is the benefit of having several instances at the same time? Is there any solution that I retrieve the old instances? – MJBZA May 17 '20 at 19:47
  • @MahdiJ.Ansari I'm not sure if this is a defect or expected angular behavior. I just tested it using setTimeout with a console.log, and yes in fact both instances continued writing to the console. I am not sure if there is a way to get back to a previous instance. This is new territory for me. – Rick May 17 '20 at 19:49
  • Maybe using a custom RouteReuseStrategy? – David May 17 '20 at 21:55
  • @David If you have any suggestion please share your idea via an answer. – MJBZA May 18 '20 at 06:27
  • @Rick Please vote up if you don't mind, then more people will attract to this question. It seems for me we do something wrong regarding navigation in angular. Because I found some info regarding `instance activation` instead of `instance creation` [here](https://stackoverflow.com/questions/48155804/angular-routing-instance-creation-vs-instance-activation). Also if we think regarding software architecture design it is stupid to generate new instance each time, use the memory and then left it behind without free up the memory!!! I don't believe Angular creators haven't think of that! – MJBZA May 18 '20 at 06:33
  • 1
    try to use `Location` from `common` https://stackoverflow.com/questions/35446955/how-to-go-back-last-page – anatoli May 18 '20 at 07:00
  • @anatoli I have seen this post already and tried Location also. However I saw the same problem! If you have any idea could you please share any answer! – MJBZA May 18 '20 at 07:04
  • 1
    have you tried to change `RouteReuseStrategy` ? https://medium.com/@juliapassynkova/angular-2-component-reuse-strategy-9f3ddfab23f5 – anatoli May 18 '20 at 07:28
  • @anatoli It seems your last comment was the correct guidance. – MJBZA May 18 '20 at 10:57

1 Answers1

3

Related to the comments you were right @anatoli, using RouteReuseStrategy will solve this issue. See here for example. Firstly we have to create a service class:

RouteReuseService.ts

import {
    RouteReuseStrategy,
    ActivatedRouteSnapshot,
    DetachedRouteHandle,
    RouterModule,
    Routes,
    UrlSegment
} from '@angular/router';
export class RouteReuseService implements RouteReuseStrategy {
    private handlers: { [key: string]: DetachedRouteHandle } = {};
    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        if (!route.routeConfig || route.routeConfig.loadChildren) {
            return false;
        }
        let shouldReuse = false;
        console.log('checking if this route should be re used or not’, route');
        if (route.routeConfig.data) {
            route.routeConfig.data.reuse ? shouldReuse = true : shouldReuse = false;
        }
        return shouldReuse;
    }
    store(route: ActivatedRouteSnapshot, handler: DetachedRouteHandle): void {
        console.log('storing handler');
        if (handler) {
            this.handlers[this.getUrl(route)] = handler;
        }
    }
    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        console.log('checking if it should be re attached');
        return !!this.handlers[this.getUrl(route)];
    }
    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
        if (!route.routeConfig || route.routeConfig.loadChildren) {
            return null;
        };
        return this.handlers[this.getUrl(route)];
    }
    shouldReuseRoute(future: ActivatedRouteSnapshot, current: ActivatedRouteSnapshot): boolean {
        let reUseUrl = false;
        if (future.routeConfig) {
            if (future.routeConfig.data) {
                reUseUrl = future.routeConfig.data.reuse;
            }
        }
        const defaultReuse = (future.routeConfig === current.routeConfig);
        return reUseUrl || defaultReuse;
    }
    getUrl(route: ActivatedRouteSnapshot): string {
        if (route.routeConfig) {
            const url = route.routeConfig.path;
            console.log('returning url', url);
            return url;
        }
    }
}

In the next step we have to make some changes in app.module.ts:

providers: [{provide: RouteReuseStrategy, useClass: RouteReuseService}],
  bootstrap: [AppComponent]

In the last step we have to set 'reuse' to true for the component, which we'd like to reuse in the application (in app-routing.modules.ts). For example here we want to reuse the DetailComponent:

const routes: Routes = [{ path: '', component: HomeComponent },
{
  path: 'detail', component: DetailComponent, data: {
    reuse: true
  }
}, { path: 'item', component: ItemComponent }];
Niels
  • 375
  • 5
  • 15