Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Service is not being singleton for angular2 router lazy loading with loadChildren

Here is the pluker. https://plnkr.co/edit/tsNlmRth4mRzz0svGWLK?p=preview

In which I have created two module with two components and one service each. I want the service should be singleton (save state) in the module level.

If you click on 'Module 1 Page 1' and 'Module 2 Page 2' It will display two different random numbers. As I am generating this number in constructor. So the service is being created each time page change. But module2 is behaving perfectly.

Note: Mode1Module is using loadChildren Mode2Module is using children

I found this same type issue is been fixed earlier as per this angular2 rc5 router service singleton

But I think still it is there. So please help me to solve this out.

like image 627
Partha Sarathi Ghosh Avatar asked Dec 05 '16 18:12

Partha Sarathi Ghosh


3 Answers

Lazy loaded modules have their own root scope. Providers added to lazy loaded modules get an instance in that root scope instead of the root scope of the application. If you add the provider to a module that is not lazy loaded, only a single instance at the application root scope will be created.

https://angular.io/docs/ts/latest/cookbook/ngmodule-faq.html#!#q-lazy-loaded-module-provider-visibility

Why is a service provided in a lazy loaded module visible only to that module?
Unlike providers of the modules loaded at launch, providers of lazy loaded modules are module-scoped.

When the Angular router lazy-loads a module, it creates a new execution context. That context has its own injector which is a direct child of the application injector.

The router adds the lazy module's providers and the providers of its imported modules to this child injector.

These providers are insulated from changes to application providers with the same lookup token. When the router creates a component within the lazy loaded context, Angular prefers service instances created from these providers to the service instances of the application root injector.

https://angular.io/docs/ts/latest/cookbook/ngmodule-faq.html#!#q-why-bad

Why is it bad if SharedModule provides a service to a lazy loaded module?
This question arose in the Angular Module chapter when we discussed the importance of keeping providers out of the SharedModule.

Suppose we had listed the UserService in the module's providers (which we did not). Suppose every module imports this SharedModule (which they all do).

When the app starts, Angular eagerly loads the AppModule and the ContactModule.

Both instances of the imported SharedModule would provide the UserService. Angular registers one of them in the root app injector (see above). Then some component injects UserService, Angular finds it in the app root injector, and delivers the app-wide singleton UserService. No problem.

Now consider the HeroModule which is lazy loaded!

When the router lazy loads the HeroModule, it creates a child injector and registers the UserService provider with that child injector. The child injector is not the root injector.

When Angular creates a lazy HeroComponent, it must inject a UserService. This time it finds a UserService provider in the lazy module's child injector and creates a new instance of the UserService. This is an entirely different UserService instance than the app-wide singleton version that Angular injected in one of the eagerly loaded components.

That's almost certainly a mistake.

Prove it for yourself. Run the live example. Modify the SharedModule so that it provides the UserService rather than the CoreModule. Then toggle between the "Contact" and "Heroes" links a few times. The username goes bonkers as the Angular creates a new UserService instance each time.

https://angular.io/docs/ts/latest/cookbook/ngmodule-faq.html#!#q-why-child-injector

Why does lazy loading create a child injector?
Angular adds @NgModule.providers to the application root injector ... unless the module is lazy loaded. Then it creates a child injector and adds the module's providers to the child injector.

This means that a module behaves differently depending on whether it is loaded during application start or lazy loaded later. Neglecting that difference can lead to adverse consequences.

Why doesn't Angular add lazy loaded providers to the app root injector as it does for eagerly loaded modules? Why the inconsistency?

The answer is grounded in a fundamental characteristic of the Angular dependency injection system. An injector can add providers until it is first used. Once an injector starts creating and delivering services, its provider list is frozen. No new providers allowed.

When an applications starts, Angular first configures the root injector with the providers of all eagerly loaded modules before creating its first component and injecting any of the provided services. Once the application begins, the app root injector is closed to new providers.

Time passes. Application logic triggers lazy loading of a module. Angular must add the lazy loaded module's providers to an injector somewhere. It can't added them to the app root injector because that injector is closed to new providers. So Angular creates a new child injector for the lazy loaded module context.

like image 78
Günter Zöchbauer Avatar answered Oct 24 '22 06:10

Günter Zöchbauer


As mentioned by @Günter Zöchbauer, this is explained the Angular docs, where lazy-loaded modules get their own instances of services. If a service is meant to be a true singleton, then you should either

Provide it directly into the AppModule

@NgModule({
  providers: [ SingletonService ]
})
class AppModule {}

These services are application-wide, even to lazy-loaded modules. So you don't need to add it to the providers of those lazy-loaded modules.

Import a module that provides the service into the AppModule

Note that a convention is to use a forRoot, as mentioned in [the docs][1]

@NgModule({
})
class CoreModule {
  forRoot() {
    return {
      ngModule: SomeModule,
      providers: [SingletonService]
    }
  }
}

@NgModule({
  imports: [ CoreModule.forRoot() ]
})
class AppModule {}

The forRoot should only be called in the AppModule imports. This will insure that only the AppModule adds the provider. Any lazy-loaded modules have access to the same singleton service provided by the AppModule.

like image 40
Paul Samsotha Avatar answered Oct 24 '22 04:10

Paul Samsotha


You can solve this problem by wrapping Mod1Page1 and Mod1Page2 components in one Mod1 component as child:

@Component({
  template: '<router-outlet></router-outlet>'
})
class Mod1Component {}

const routes: Routes = [
  {
    path: '',
    component:Mod1Component,
    children:[
      {
        path : 'page1',
        component : Mod1Page1Component
      },
      {
        path : 'page2',
        component : Mod1Page2Component
      }
    ]
  }
];

const routedComponents = [Mod1Component,Mod1Page1Component, Mod1Page2Component];

Plnkr link to solved fork

Mod1Service will be created every time when you navigate to any Mod1Module's component from component of any other module, but when you navigate beetwen Mod1Module's component service will not be recreated.

like image 2
alezhu Avatar answered Oct 24 '22 05:10

alezhu