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

3

30

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.

Matronymic answered 5/12, 2016 at 18:53 Comment(5)
Is it working? What is the problem in your plunker? I saw the same message in pages 1 and 2 and I think that is the right behaviour, isn't it? Are you having another problem?Pless
If you check Page 1, Page 2 for module 1. It is different. But for Module 2 is same. Where as Both the module's Page 1 and Page 2 will be same.Matronymic
You are right, the problem still there and we can see it in your plunker. I've tested locally, with angular-cli and the bug persistis. You can place a message in this issue github.com/angular/angular/issues/11125 include your plunker link so they can see the bug. Reference to this SO Question and say that we've tested with system.js and angular-cli. Thanks!Pless
The NgModule FAQ clearly states that this is not supported. if you need this, then just don't add the provider to the lazy loaded module but instead add it to an eager loaded module.Satterlee
If this is the scenario then, "sharing data in lazy loaded module only" is clearly not possible via shared services. Any way we need to use eager loaded module for it. Am I right?Matronymic
W
42

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.

Whitehall answered 5/12, 2016 at 19:23 Comment(7)
Then how to share state data between lazy loaded module components?Matronymic
Lazy loaded modules get the global services injected as well. Just provide the services at a non-lazy loaded module. Just the other way around doesn't work. Services of lazy loaded modules are only available for components and services in this module and other modules imported by them but not to non-lazy-loaded or other laty-loaded modulSatterlee
that means if we import service in app.module.ts than it will available for all lazy loaded or non-lazy loaded module?? @GünterZöchbauerLepidopteran
Ecactly . . . . .Satterlee
@GünterZöchbauer - It means a lazy loaded module will first look for the dependency in it's own root context & if it doesn't find it there then it will look into the application's root context. That's why we just call forRoot() in root module to import providers & don't import them again in lazy loaded modules. Please correct me if I am wrong.Woozy
@AnkushJain exactly. This way you ensure there will only be a single instance for these providersSatterlee
Seems now the preferred way is just @Injectable({ providedIn: 'root' }) ?Devora
A
32

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 {
  static 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.

Amazonite answered 23/12, 2016 at 1:56 Comment(3)
MyService should be singleton but not throught out the app. Rather through out the module where it is defined. I want to archive that only. Suppose I am creating a loosely coupled application with different module. Each module is independent with other. While integrating all module to my App I do not want to put all singleton Service to my AppModule.Matronymic
Looking at the last sample, what if coremodule is declared in a lazy loaded module? If I do coremodule then I must import it so its not lazy ...in other words - can I use forroot for lazy loaded modules?Curry
For reference, here is the link to the relevant page in the Angular docs: angular.io/guide/singleton-services#the-forroot-patternNaturalist
T
2

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.

Tapetum answered 26/1, 2017 at 8:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.