Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 4 are services provided in a core module singleton for real?

I'm trying to understand core module and singleton services in angular 4. The official documentation (https://angular.io/guide/ngmodule) says the following things:

UserService is an application-wide singleton. You don't want each module to have its own separate instance. Yet there is a real danger of that happening if the SharedModule provides the UserService.

CoreModule provides the UserService. Angular registers that provider with the app root injector, making a singleton instance of the UserService available to any component that needs it, whether that component is eagerly or lazily loaded.

We recommend collecting such single-use classes and hiding their details inside a CoreModule. A simplified root AppModule imports CoreModule in its capacity as orchestrator of the application as a whole.

import { CommonModule }      from '@angular/common';
import { TitleComponent }    from './title.component';
import { UserService }       from './user.service';
import { ModuleWithProviders, NgModule, Optional, SkipSelf }       from '@angular/core';
@NgModule({
  imports:      [ CommonModule ],
  declarations: [ TitleComponent ],
  exports:      [ TitleComponent ],
  providers:    [ UserService ]
})
export class CoreModule {
    constructor (@Optional() @SkipSelf() parentModule: CoreModule) { ... }
}

So I'm using the Core Module providing singleton services, and the constructor

constructor (@Optional() @SkipSelf() parentModule: CoreModule) { ... }

prevent to import the Core Module more than one time.

1) BUT, what if I provide the UserService in another module (e.g in a lazy-loading module) ? This lazy-loaded module has a new instance of the service?

And about forRoot method:

@NgModule({
  imports:      [ CommonModule ],
  providers:    [ UserService ]
})
export class CoreModule {
}

static forRoot(config: UserServiceConfig): ModuleWithProviders {
  return {
    ngModule: CoreModule,
    providers: [
      {provide: UserServiceConfig, useValue: config }
    ]
  };
}
}

2) If I import the CoreModule using CoreModule.forRoot() in the AppModule, what happen to the UserService ? Is it provided too?

Thanks

like image 772
user2010955 Avatar asked Mar 09 '23 13:03

user2010955


2 Answers

The documentation is confusing, particularly this line:

UserService is an application-wide singleton. You don't want each module to have its own separate instance. Yet there is a real danger of that happening if the SharedModule provides the UserService.

There's no danger of that happening if you don't use lazy loaded modules. Let's see an example. You have A module that imports B module. Both modules define providers:

@NgModule({
   providers: {provide: 'b', 'b'}
})
export class BModule {}

@NgModule({
   imports: [AModule]
   providers: {provide: 'a', 'a'}
})
export class AModule {}

What happens when the compiler generates a module factory is that it merges these providers together and factory only for one module will be created. Here is how it will look:

var AModuleNgFactory = jit_createNgModuleFactory0(

    // reference to the module class
    jit_AppModule1,  

    // array of bootstrap components
    [jit_AppComponent2],

    function (_l) {
        return jit_moduleDef3([

            // array of providers
            jit_moduleProvideDef4(256, 'b', 'b', []),
            jit_moduleProvideDef4(256, 'a', 'a', [])
            ...,
        ]);

You can see that the providers are merged. Now, if you define two modules with the same provider token, the modules will be merged and the providers from the module that imports the other will override the imported module providers:

@NgModule({
   providers: {provide: 'a', 'b'}
})
export class BModule {}

@NgModule({
   imports: [AModule]
   providers: {provide: 'a', 'a'}
})
export class AModule {}

The factory definition will look like this now:

function (_l) {
    return jit_moduleDef3([

        // array of providers
        jit_moduleProvideDef4(256, 'a', 'a', []),
        ...,
    ]);

So no matter how many modules you import, only one factory with merged providers is created. And only one root injector is created. The injector that components create is not "real" injector - check this answer to understand why.

This lazy-loaded module has a new instance of the service?

When it comes to lazy loaded modules, Angular generates separate factories for them. It means that providers defined in them are not merged into the main module injector. So if a lazy loaded module defines the provider with the same token, Angular will create new instance of that service even if there's already one in the main module injector.

If I import the CoreModule using CoreModule.forRoot() in the AppModule

To understand what forRoot does, see RouterModule.forRoot(ROUTES) vs RouterModule.forChild(ROUTES).

like image 75
Max Koretskyi Avatar answered Mar 11 '23 04:03

Max Koretskyi


1) Yes. This relates to the fact that the dependency injector is hierarchical.

This means, every module has a set of elements that can be injected (module level), and if one of its elements requires a dependency which doesn't isnt present at a module level, then the dependency injector will look for the dependency in the parent of the module (the one module that imported it), and so on until it finds the dependency or reaches the root (app.module), where it will throw an error if the dependency cant be resolved (hierarchy level).

2) Yes, the UserService will be provided. forRoot will create a different "version" of the CoreModule, in which the "normal" CoreModule is expanded with the extra properties that you add.

In most cases, forRoot will take the "normal" version of the module and include the providers array, to ensure that services are singleton. The "normal" version of the module, will only have components, pipes or other non singletone elements.

Take as example the TranslateModule of ngx-translate (extracted relevant section):

@NgModule({
    declarations: [
        TranslatePipe,
        TranslateDirective
    ],
    exports: [
        TranslatePipe,
        TranslateDirective
    ]
}) // this defines the normal version of the module
export class TranslateModule {
    static forRoot(): ModuleWithProviders { // this kinda tells "module + providers"
        return {
            ngModule: TranslateModule, // take the normal version
            providers: [ // merge this to the providers array of the normal version
                TranslateStore,
                TranslateService
            ]
        };
    }
}

Maybe this resource could be useful as further explanation: https://www.youtube.com/watch?v=8VLYjt81-fE

like image 38
Jota.Toledo Avatar answered Mar 11 '23 04:03

Jota.Toledo