2
votes

I was looking at the official angular documentation about NgModule (https://angular.io/guide/ngmodule#configure-core-services-with-coremoduleforroot) and in the example provided at the end of the page there's the file core.module.ts

import { ModuleWithProviders, NgModule, Optional, SkipSelf }       from '@angular/core';
import { CommonModule }      from '@angular/common';
import { TitleComponent }    from './title.component';
import { UserService }       from './user.service';
import { UserServiceConfig } from './user.service';

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

  constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }

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

This is a Core Module, and by using the constructor and the forRoot method we're sure the core module is imported only once, and we have only one instance of the services provided.

  • What's the difference between the providers array in the @NgModule({...}) and the providers array in the method forRoot?
  • Should I provide my singleton services in the @NgModel section or in the Core Module?
2

2 Answers

2
votes

forRoot is used for creating singletons.

You should be calling forRoot() on the root level only (AppModule).

With this structure:

@NgModule({
    imports: [
        CommonModule
    ],
    declarations: [],
    exports: [],
    providers: []

})
export class I18nModule {
    static forRoot() {
        return {
            ngModule: I18nModule,
            providers: [I18nService, UserConfig]
        };
    }
}

You will have to import I18nModule (just an example) like this in AppModule:

I18nModule.forRoot()

And like this in the rest of imports:

imports: [I18nModule]

Basically, on the AppModule import you are instancing singletons of the services that are in the providers of the forRoot() imported module. This allows you to always reference the same instance instead of having several instances of the same service.

Why 2 providers arrays:

In your question, the first array could perfectly be empty, adding all your providers into your forRoot() providers array. So you could:

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

  constructor (@Optional() @SkipSelf() parentModule: CoreModule) {
    if (parentModule) {
      throw new Error(
        'CoreModule is already loaded. Import it in the AppModule only');
    }
  }

  static forRoot(config: UserServiceConfig): ModuleWithProviders {
    return {
      ngModule: CoreModule,
      providers: [
        UserService,
        {provide: UserServiceConfig, useValue: config }
      ]
    };
  }
}
  • You should provide your singleton services in your CoreModule and import it with forRoot() in your AppModule
  • If you are using forRoot(), add your providers in your forRoot method
2
votes

This is a Core Module, and by using the constructor and the forRoot method we're sure the core module is imported only once, and we have only one instance of the services provided.

Technically it doesn't ensure that it's only once. The constructor throws an error if there is a duplicate. Resolving the duplicate error becomes the developer's responsibility, and sometimes that can be difficult.

What's the difference between the providers array in the @NgModule({...}) and the providers array in the method forRoot?

The @NgModule creates metadata that is attached to the CoreModule class. Angular uses this metadata to load the module. The forRoot is a function which returns metadata as an array. That means you don't have the help of a decorator to format that array and have to do it manually.

The providers in the forRoot declare that a dependency symbol is associated with a value. That value is passed to the function as a parameter.

You can actually do this with @NgModule and get the same result.

 let config = new UserServiceConfig(); // <-- create instance here.

 @NgModule({
     imports:      [ CommonModule ],
     declarations: [ TitleComponent ],
     exports:      [ TitleComponent ],
     providers:    [ 
         UserService,
         {provide: UserServiceConfig, useValue: config }
     ]
 })

The above does the same thing as forRoot() but it introduces two problems.

  • the above code could be executed more than once
  • how would a module know what configuration it needs?

Should I provide my singleton services in the @NgModel section or in the Core Module?

I want to say that there are no singletons in Angular. That's because the dependency injection tree doesn't support such a classification of a provider. To get around this limitation we add this forRoot trick to declare a provider only once for one particular import.

This is the key to understanding how this works. We're not altering what is provided. We are altering what is imported.

It means that we want only one module to import this service. It's assumed that the module doing the importing is only loaded once. Therefore, this creates a singleton of that provider.

What's important is that your main module call forRoot() in it's imports section, and this function is only used once.

The tutorial you read uses this technique to create only one provider of a config object of type UserServiceConfig. You can then inject this type into your other objects.