Services providing

Services providing

I will cover 3 use cases when it comes to providing services in our app:

  • Singleton services.
  • Same instance of a service for eager modules and a different instance of the same service for each lazy loaded module.
  • Component-specific instance of a service.

Each use case is in this angular 5 github repository or on this angular 14 github repository, on the specified branch.

Singleton services (singleton-service branch):

Singleton services are services for which only one instance exists in the entire application. While the tab is not closed or refreshed these services can handle some state of the application. There are two methods to create such singleton services:

  • Adding to the providers array on NgModule only in the root module (Angular 2+):

We can provide a service on AppModule and simply pass it into the component's constructor where we want to use it.

import { Injectable } from "@angular/core";
console.log("%cSharedService" + "%c bundled", "color:red", "color:green");
@Injectable()
export class SharedService {
  constructor() {
    console.log("%cSharedService" + "%c instantiated", "color:red", "color:green");
  }
}

First console.log will be visible when the SharedService is bundled and the second one when it is instantiated (as many times as it is).

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { EagerLoadedComponent } from './eager-loaded/eager-loaded.component';
import { SharedService } from './shared/shared.service';


@NgModule({
  declarations: [
    AppComponent,
    EagerLoadedComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [SharedService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Here we are providing SharedService into the root Module, this is, AppModule.

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-eager-loaded',
  templateUrl: './eager-loaded.component.html',
  styleUrls: ['./eager-loaded.component.css']
})
export class EagerLoadedComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }

}

Here we pass SharedService into the EagerLoadedComponent constructor.

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-lazy-loaded',
  templateUrl: './lazy-loaded.component.html',
  styleUrls: ['./lazy-loaded.component.css']
})
export class LazyLoadedComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }

}

And here we pass SharedService into the LazyLoadedComponent constructor.

The recommended pattern for doing this and ensuring that we use only one instance of SharedService is the forRoot pattern:

import {
  ModuleWithProviders,
  NgModule,
  Optional,
  SkipSelf,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { SharedService } from "./shared.service";

@NgModule({
  imports: [CommonModule],
  declarations: [],
})
export class SharedModule {
  constructor(@Optional() @SkipSelf() parentModule: SharedModule) {
    if (parentModule) {
      throw new Error(
        "SharedModule is already loaded. Import it in the AppModule only"
      );
    }
  }
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [SharedService],
    };
  }
}

The constructor part ensures that this module will only be imported once (on root module as mentioned before). @Optional means that not finding an existing SharedModule instance in the importing module injector or above it in the injector hierarchy is not a problem, @SkipSelf means that a SharedModule instance should only be detected if it exists above the importing injector in the injector hierarchy. If these two conditions are not met an error will be thrown. static forRoot() allows separating providers from a module to import them into the root module as explained before.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';
import { AppRoutingModule } from './app-routing.module';
import { EagerLoadedComponent } from './eager-loaded/eager-loaded.component';


@NgModule({
  declarations: [
    AppComponent,
    EagerLoadedComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    SharedModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We simply import SharedModule calling forRoot() method.

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-eager-loaded',
  templateUrl: './eager-loaded.component.html',
  styleUrls: ['./eager-loaded.component.css']
})
export class EagerLoadedComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }

}

In the same way as before we pass it into the components constructor

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-lazy-loaded',
  templateUrl: './lazy-loaded.component.html',
  styleUrls: ['./lazy-loaded.component.css']
})
export class LazyLoadedComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }

}

Captura de Pantalla 2022-06-19 a las 21.11.00.png

Here we see that when we just launch the application the only log shown is that the Shared service is bundled. It is bundled because we provide it in both cases in the root module.

Captura de Pantalla 2022-06-19 a las 21.11.17.png

In this screenshot we see that when we click to access the Eager module it instantiated once the SharedService.

Captura de Pantalla 2022-06-19 a las 21.11.37.png

In this screenshot we see that although we previously instantiated the SharedService once and that although we are requesting a lazy-loaded module that injects SharedService, it is not instantiating again. We continue seeing only one log.

  • Use of providedIn: 'root' (Angular 6+).

This is the easiest way to create a singleton service from Angular version 6 onwards, version 6 included.

import { Injectable } from '@angular/core';

console.log("%cSharedService" + "%c bundled", "color:red", "color:green");
@Injectable({
  providedIn: 'root'
})
export class SharedService {

  constructor() {
    console.log("%cSharedService" + "%c instantiated", "color:red", "color:green");
  }
}

Here we specify the providedIn property to be "root" and angular will provide us with a singleton service for the entire application.

This way of creating a singleton service is preferable to adding the service to the providers array in NgModule of the root module because it uses tree shaking. This means that if the SharedService is not passed into any constructor of any component it would not be bundled, this minifies our code.

If we pass SharedService to both components, Eager one and Lazy one:

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-eager-loaded',
  templateUrl: './eager-loaded.component.html',
  styleUrls: ['./eager-loaded.component.scss']
})
export class EagerLoadedComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }

}
import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-lazy-loaded',
  templateUrl: './lazy-loaded.component.html',
  styleUrls: ['./lazy-loaded.component.scss']
})
export class LazyLoadedComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }
}

Captura de Pantalla 2022-06-20 a las 11.13.14.png

We will see that as the method followed with providers array in Ngmodule of the root module we only have one instance of SharedService:

However, if we don't pass SharedService to the constructor of the EagerLoadedComponent:

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-eager-loaded',
  templateUrl: './eager-loaded.component.html',
  styleUrls: ['./eager-loaded.component.scss']
})
export class EagerLoadedComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

Captura de Pantalla 2022-06-20 a las 11.15.20.png

We will see that SharedService is nor bundled

Captura de Pantalla 2022-06-20 a las 11.16.16.png

But if we click to request the lazy loaded module, it will be bundled and instantiated as shown in this screenshot. You can open the network tab to see that I this case SharedService is bundled with the LazyLoadedModule and in the previous one with the main module.

Same instance of a service for eager modules and a different instance of the same service for each lazy loaded module (eager-lazy branch).

  • Adding to the providers array on NgModule only in the root module (Angular 2+) and to the providers array on NgModule on each lazy-loaded module:

Captura de Pantalla 2022-06-20 a las 11.55.32.png

We have these folder structure

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { EagerLoadedOneComponent } from './eager-loaded-one/eager-loaded-one.component';
import { EagerLoadedTwoComponent } from './eager-loaded-two/eager-loaded-two.component';
import { SharedService } from './shared/shared.service';


@NgModule({
  declarations: [
    AppComponent,
    EagerLoadedOneComponent,
    EagerLoadedTwoComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
  ],
  providers: [SharedService],
  bootstrap: [AppComponent]
})
export class AppModule { }

As singleton service we add to providers array of NgModule in root module the SharedService. This service will have an unique shared instance for all the EagerLoaded components

Captura de Pantalla 2022-06-20 a las 11.57.56.png

If we click on both links to access EagerLoded components we will see that it is only created one instance of the SharedService

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyLoadedOneComponent } from './lazy-loaded-one.component';
import { LazyLoadedOneRoutingModule } from './lazy-loaded-one-routing.module';
import { SharedService } from '../shared/shared.service';

@NgModule({
  imports: [
    CommonModule,
    LazyLoadedOneRoutingModule,
  ],
  declarations: [LazyLoadedOneComponent],
  providers: [SharedService]
})
export class LazyLoadedOneModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyLoadedTwoRoutingModule } from './lazy-loaded-two-routing.module';
import { LazyLoadedTwoComponent } from './lazy-loaded-two.component';
import { SharedService } from '../shared/shared.service';

@NgModule({
  imports: [
    CommonModule,
    LazyLoadedTwoRoutingModule,
  ],
  declarations: [LazyLoadedTwoComponent],
  providers: [SharedService]
})
export class LazyLoadedTwoModule { }

In this case, as mentioned we add to the providers array of the NgModule of each LazyLoaded module the SharedService

Captura de Pantalla 2022-06-20 a las 12.00.35.png

If we click on both links of LazyLoaded modules as we want we will that only two instances are created.

This would meet our goal but as previously mentioned, we will use the forRoot pattern to inform our development in a deeper level and we will create a forChild method for each LaZyLoaded module:

import {
  ModuleWithProviders,
  NgModule,
} from "@angular/core";
import { CommonModule } from "@angular/common";
import { SharedService } from "./shared.service";

@NgModule({
  imports: [CommonModule],
  declarations: [],
})
export class SharedModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [SharedService],
    };
  }
  static forChild(): ModuleWithProviders {
    return {
      ngModule: SharedModule,
      providers: [SharedService],
    };
  }
}

forRoot and forChild method return the same object, it is only a matter of conceptually separating one from the other.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { EagerLoadedOneComponent } from './eager-loaded-one/eager-loaded-one.component';
import { EagerLoadedTwoComponent } from './eager-loaded-two/eager-loaded-two.component';
import { SharedService } from './shared/shared.service';
import { SharedModule } from './shared/shared.module';


@NgModule({
  declarations: [
    AppComponent,
    EagerLoadedOneComponent,
    EagerLoadedTwoComponent
  ],
  imports: [
    BrowserModule,
    SharedModule.forRoot(),
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We import the SharedModule calling forRoot method

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyLoadedOneComponent } from './lazy-loaded-one.component';
import { LazyLoadedOneRoutingModule } from './lazy-loaded-one-routing.module';
import { SharedService } from '../shared/shared.service';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  imports: [
    CommonModule,
    SharedModule.forChild(),
    LazyLoadedOneRoutingModule,
  ],
  declarations: [LazyLoadedOneComponent],
  providers: []
})
export class LazyLoadedOneModule { }
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyLoadedTwoRoutingModule } from './lazy-loaded-two-routing.module';
import { LazyLoadedTwoComponent } from './lazy-loaded-two.component';
import { SharedService } from '../shared/shared.service';
import { SharedModule } from '../shared/shared.module';

@NgModule({
  imports: [
    CommonModule,
    SharedModule.forChild(),
    LazyLoadedTwoRoutingModule,
  ],
  declarations: [LazyLoadedTwoComponent],
  providers: []
})
export class LazyLoadedTwoModule { }

And we do the same on each LazyLoaded module but calling forChild method to follow the pattern.

Captura de Pantalla 2022-06-20 a las 12.14.18.png

In this case, the result of clicking all the links is the same as without the forRoot pattern as expected, a total of three instances.

  • Use of providedIn: 'any' (Angular 6+).

Captura de Pantalla 2022-06-20 a las 12.37.59.png

This is the folder structure that we use.

We achieve to have same instance on EagerLoaded modules and different one on each LazyLoaded module just setting providedIn: 'any':

import { Injectable } from '@angular/core';

console.log("%cSharedService" + "%c bundled", "color:red", "color:green");
@Injectable({
  providedIn: 'any'
})
export class SharedService {

  constructor() {
    console.log("%cSharedService" + "%c instantiated", "color:red", "color:green");
  }
}

As simply as this we achieved the goal.

Captura de Pantalla 2022-06-20 a las 12.36.39.png

After clicking as many times as we want all the links we will see that we have 3 instances of SharedService, as using the previous method.

Component-specific instance of a service (component-specific branch).

The downside of this use case is that the service will only be alive while the component is alive. Here we will use the same code as in the previous use case and this works using the forRoot pattern, not using it and in conjunction with providedIn: 'root' or providedIn: 'any'. Suppose that instead of requiring the same instance for all EagerLoaded components, we want one EagerLoaded component to have a different instance.

import { Component, OnInit } from '@angular/core';
import { SharedService } from '../shared/shared.service';

@Component({
  selector: 'app-eager-loaded-one',
  templateUrl: './eager-loaded-one.component.html',
  styleUrls: ['./eager-loaded-one.component.css'],
  providers: [SharedService]
})
export class EagerLoadedOneComponent implements OnInit {

  constructor(private sharedService: SharedService) { }

  ngOnInit() {
  }

}

We simply include SharedService in the providers array of the Component we want to have a unique instance of SharedService.

Captura de Pantalla 2022-06-20 a las 13.28.27.png

After clicking every link as many times as we want we will see that four instances of SharedService have been created but sometimes an instance is destroyed. What is happening is that every time we click on Eager Loaded Module One we create an instance and every time we click on another link this instance is destroyed so when clicking again on Eager Loaded Module One link a new instance will be created.

Bibliography: