Search code examples
angularangular-routing

Prevent Angular to construct a new instance of a component when revisiting a route (Route Reuse Strategy)


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();
            }
        }
    }

Solution

  • 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 }];