{ "community_link": "https://community.indepth.dev/t/angulars-root-and-any-provider-scopes" }

A detailed look at Angular's 'root' and 'any' provider scopes

Angular 9 Ivy provides us with a few more options when defining Provider Scope. In this article we will see why 'root' is not sufficient and how the new value 'any' can help.

A detailed look at Angular's 'root' and 'any' provider scopes

If you are following Angular 9 release, you may have heard how providedIn has few more properties, module scope has been possible since version 2, tree-shakable module scope became available in version 6 with providedIn: MyServiceModule and now we have 'any' and 'platform' .

Tweet by Manfred

In this blog post we will see the example why we need 'any' and how it is useful. We will leave 'platform' for our next blog post.

Tree-Shakable Providers

In Angular 6 providedIn property was added to providers, to make services tree-shakable. If you are new to Angular, let me give you an simple explanation what we mean by tree shaking - it is process to remove, the unused code from our application. It means if you create a service but do not use it, in your application the code will not be part of final production build. To learn more you can refer this blog-post from Lars.

Why the any scope?

So we now know why 'root' was introduced, the idea was to make services tree-shakable. To understand providedIn: 'any' we have to talk a little bit about implementation of forRoot and forChild and lazy loading. If you have used Angular Router or NgRx then you know about these methods.

The problem of working with lazy loaded module is that if we use providedIn: 'root' even though we think we should get a new instance of a service, it gives the same instance and that might not be the behavior we expect. When working with a lazy-loaded module, a new instance should be created when we load the module.

Let’s write some code to see, what was the issue earlier and how 'any' (make sure you don’t miss the quotes to avoid confusion with any type from typescript) resolves that for us.

What we are going to achieve

  • A config service which will take some config parameter apiEndpoint and timeout.
  • 2 lazy loaded modules employee and department, they want to use the config service, but with different values.

The problem with using the root scope

Create a new Angular 9 app using the below command if you don't want to install Angular 9 globally

npx -p @angular/cli ng new providerdemo

If you have Angular CLI 9 installed globally skip npx -p @angular/cli from all commands.

Now let’s create 2 new lazy loaded modules with components, run the below commands:

npx -p @angular/cli ng g module employee --routing --route employee --module app

npx -p @angular/cli ng g module department --routing --route department --module app

Create a new value provider, and an interface, you can add it in a new shared folder, as this code will be shared between multiple modules:

export interface Config {
  apiEndPoint: string;
  timeout: number;
}
demo.config.ts
import { InjectionToken } from '@angular/core';
import { Config } from './demo.config';

export const configToken = new InjectionToken<Config>('demo token');
demo.token.ts

Next, create a new service we will call it ConfigService which will read the value from token and use it to perform some operation. Use the below command to create it:

npx -p @angular/cli ng g service shared/config

Once created, add the below code to your service:

import { Injectable, Inject } from '@angular/core';
import { configToken } from './demo.token';
import { Config } from './demo.config';

@Injectable({
  providedIn: 'root'
})
export class ConfigService {

  constructor(@Inject(configToken) private config: Config) {
    console.log('new instance is created');
  }

  getValue() {
    return this.config;
  }
}
config.service.ts

Now let’s use this service in Employee and Department Component we created with their respective modules. We are just printing the values received from Token:

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

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

  constructor(private configService: ConfigService) { }

  ngOnInit(): void {
    console.log(this.configService.getValue());
  }
}
employee.component.ts
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '../shared/config.service';

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

  constructor(private configService: ConfigService) { }

  ngOnInit(): void {
    console.log(this.configService.getValue());
  }
}
department.component.ts

Next, let’s try to pass 2 different configuration for Employee and Department, add the below code into both the modules:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { EmployeeRoutingModule } from './employee-routing.module';
import { EmployeeComponent } from './employee.component';
import { Config } from '../shared/demo.config';
import { configToken } from '../shared/demo.token';

export const configValue: Config = {
  apiEndPoint: 'abc.com',
  timeout: 3000
};


@NgModule({
  declarations: [EmployeeComponent],
  imports: [
    CommonModule,
    EmployeeRoutingModule
  ],
  providers: [{
    provide: configToken, useValue: configValue
  }]
})
export class EmployeeModule {
  constructor() { }
}
employee.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { DepartmentRoutingModule } from './department-routing.module';
import { DepartmentComponent } from './department.component';
import { Config } from '../shared/demo.config';
import { configToken } from '../shared/demo.token';

export const configValue: Config = {
  apiEndPoint: 'xyz.com',
  timeout: 4000
};


@NgModule({
  declarations: [DepartmentComponent],
  imports: [
    CommonModule,
    DepartmentRoutingModule
  ],
  providers: [{
    provide: configToken, useValue: configValue
  }]
})
export class DepartmentModule { }
department.module.ts

The difference as visible is the config values. Lets run  the application and see what we get, remember the providedIn value is still 'root' .To test the application in current state let’s add the routes to app.component.html, add the below snippet in app.component.html:

<a routerLink="employee">Employee</a>
<br>
<a routerLink="department">Department</a>
<router-outlet></router-outlet>
app.component.html

Next, run the application using the below command:

npx -p @angular/cli ng serve -o

The application works. But click on one of the routes, and boom, we have an error. Our expectation was to see the different config values, but we have the error:

provider-error

So what went wrong here?

We did everything right, our expectation was to get different config values for both employee and department component, but we ended up getting an error. This is due to providedIn: 'root' value. This diagram demonstrates what happened:

Figure 1. Root provider scope.

When we provide the service with providedIn: 'root', it is registered with our AppModule. As soon as we tried to activate one of the routes, the service expected the config value which was not provided. So let’s make it work by making changes to our AppModule.

Add the below code to your app.module.ts

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { Condfig } from './shared/demo.config';
import { configToken } from './shared/demo.token';


export const configValue: Config = {
  apiEndPoint: 'def.com',
  timeout: 5000
};

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [{
    provide: configToken, useValue: configValue
  }],
  bootstrap: [AppComponent]
})
export class AppModule { }
app.module.ts

Now the application works, and click on routes, we will just see below 2 values for both employee and department component. Which is expected as per Image 1.

apiEndPoint: 'def.com'
timeout: 5000

What we wanted to achieve

Figure 2. Injector provider scope.

In reality we wanted something like Image 2, where each module has their own instance, but with providedIn: 'root' this was not possible. To resolve this issues, the previous solution was implementing forRoot and forChild static methods, so each component can have their own instances.

The other way to achieve this was to provide the ConfigService in each and every module, but the problem is service is not tree shakable any more.

@NgModule({
  providers: [
    ConfigService,
    {
    provide: CONFIG_TOKEN, useValue: CONFIG_VALUE
  }]
}
export class EmployeeModule { }

Now let’s change the providedIn property for ConfigService to below:

@Injectable({
  providedIn: 'any'
})

Run the application again and notice the console now:

Final App

Bingo! We got the separate instances, without implementing forRoot or forChild static methods, or compromising with tree shakable providers.

So what happened after changing the providedIn: 'any', as shown by the image below, now all the eager loaded module will share one common instance and all lazy loaded module will have their own instance of ConfigService.

Figure 3. Any provider scope.

Conclusion

Earlier it was a challenge to ensure that you'll get a new instance of a service for lazy loaded modules. Now with the new value 'any' it’s easy to achieve. We can provide any tokens and developers can provided the values per lazy loaded module and the service will always create a new instance per lazy loaded module.

You can download the code for this project from GitHub.