In this article I’m showing some fundamentals of Angular Hierarchical Dependency Injection feature and why and when to use forRoot()
providers.
Learning Angular can be tricky, confusing and frustrating. First thing is that it requires to learn a new language - TypeScript. However, this technology for someone working with strongly-typed languages looks clean and can even be perceived as the first finally well done thing on the client side. But Angular… hundreds of pages of quick-intro tutorial speak for itself.
For example if you want to create just a form, prepare yourself to read almost 34 pages of text to start. The same about the Dependency Injection mechanism - probably hundred or more pages on my monitor explaining: how, why, what if and why if not. All this just to show then a few examples where the things you’ve just learned don’t work, revealing the intricacies of the internal implementation to explain why and in which scenarios they don’t work plus how you can workaround it in your app.
I’m just really glad that these guys didn’t write any Java framework that became popular and I need to write backend in this :)
So, returning to the Dependency Injection… It was hard to understand for me why it is so complex in Angular, that requires 100 pages of manual and why you then need to hack it with forRoot()
providers, while the same thing in Spring or Micronaut works just out of the box, in the background, and you don’t need to think much about it. After delving into the subject I must say I still don’t know why it needs to be so complicated, but at least understand better how it works.
Considering such modules and components diagram:
Example application structure
If LazyModule
is lazily loaded with a router, there’s a new dependency injector created for it. So in the application above we have two different injectors: root (at AppModule
level; exists in every Angular app), and LazyModule injector. Each injector can create its own service instances of the same service and you may think you have singleton services, while there are multiple distinct instances of them used by different modules.
We can write such a simple guard to verify if the service is unique:
export class SharedService {
val: number;
constructor() {
this.val = Math.round(Math.random() * 1000000);
console.log(`Creating shared service: ${this.val}`)
}
}
And this simple shared service can be provided by some shared module:
@NgModule(
providers: [SharedService]
)
export class SharedModule { }
Let’s now try to inject our shared service into AppComponent
and LazyComponent
:
@Component(/*...*/) // belongs to AppModule
export class AppComponent {
constructor(
public sharedService: SharedService
}
}
@Component(/*...*/) // belongs to LazyModule
export class LazyComponent {
constructor(
public sharedService: SharedService
}
}
Remember, that the AppComponent
belongs to the AppModule
, while the LazyComponent
belongs to the LazyModule
. We also have now another SharedModule
which provides only one service called SharedService
. So, our app diagram looks now as follows:
Example application structure
After running this app we get the error, because the SharedService
is not know to the dependency injection mechanism: it’s provided by the SharedModule
which is not imported by anyone here. To get things done we need to import our SharedModule
into the AppModule
:
@NgModule({
imports: [
SharedModule
]
})
export class AppModule { }
And here is what we get in the logs:
Creating shared service: 683434
This is a first important lesson about the dependency injection in Angular: services imported in the root injector (AppModule
) are becoming available for all child application modules. Like for example here LazyComponent
uses the same 683434
instance of the SharedService
because it is already accessible from the root injector (no additional service has been created in the logs).
But if we import the same SharedModule
into the LazyModule
:
@NgModule({
imports: [
SharedModule
]
})
export class LazyModule { }
Here is what we get:
Creating shared service: 116681
Creating shared service: 918498
This is a second important lesson: the lazily initialized module creates its own injector which provides a new service instance for all modules imported by this module. Like for example here, because we import the SharedModule
again in the LazyModule
, the LazyModule injector creates another instance of the SharedService
. So, we don’t have anymore the singleton SharedService
in this app.
Why would we want to import the SharedModule
again in the LazyModule
, while it already has been imported in the AppModule
? At least in those two scenarios:
SharedModule
introduces some components which the LazyModule
wants to use, they won’t be accessible to the LazyModule
until it imports the SharedModule
explicitly. Components imported by the AppModule
are not inherited by the LazyModule
- only services are inherited (this is main Angular dependency injection design flaw in my opinion, causing all this trouble I’m writing about).LazyModule
might be reusable. This means someone could write this module without knowing in what app it will be used, and what will be imports
of this app AppModule
.forRoot()
for?forRoot()
in Angular is something you can frequently see in the AppModule
imports, for example in you use ngx-bootstrap:
@NgModule({
imports: [
ModalModule.forRoot(),
ButtonsModule.forRoot(),
BsDropdownModule.forRoot(),
ProgressbarModule.forRoot(),
AlertModule.forRoot()
]
})
export class AppModule {}
And this is just Angular’s workaround for the problem described above: how to ensure that our SharedService
will be a singleton service for every app?
This is just done by not providing the SharedService
by the SharedModule
, and giving instead static forRoot()
function which does provide it instead:
@NgModule(
// providers: [SharedService]
)
export class SharedModule {
public static forRoot() {
return {ngModule: SharedModule, providers: [SharedService]};
}
}
So now, if the LazyModule
imports the SharedModule
it won’t create its own SharedService
instance, because this service is no longer provided by the SharedModule
. However, to get the SharedService
injected into the AppModule
injector (and the AppModule
injector only), we need to import SharedModule.forRoot()
:
@NgModule({
imports: [
SharedModule.forRoot()
]
})
export class AppModule { }
Having this done we have exactly the same configuration as in the very first case described in this article: the AppModule
provides the SharedService
, while the LazyModule
doesn’t override it with its own instance.
In Angular 7 there’s a new construction introduced, however the old syntax should be known as well by developers, because a plenty of external libs use it. In the new syntax we don’t use providers
on @NgModule
:
export class SharedService {}
@NgModule(
providers: [SharedService]
)
export class SharedModule { }
But, instead we use @Injectable(providedIn)
on the service:
@Injectable({providedIn: SharedModule})
export class SharedService {}
@NgModule()
export class SharedModule { }
Both two versions above are syntactically equivalent, and they mean the SharedService
is provided by the SharedModule
. This is the configuration we can face the problem with the secondary SharedService
instance created by the LazyModule
injector.
However, this construction eliminates the necessity of using forRoot()
workaround, because now you can say in @Injectable(providedIn)
that the specific service can only be injected into the root injector:
@Injectable({providedIn: 'root'})
export class SharedService {}
@NgModule()
export class SharedModule { }
With this declaration you can import the SharedModule
into our both modules:
@NgModule({
imports: [
SharedModule
]
})
export class AppModule { }
@NgModule({
imports: [
SharedModule
]
})
export class LazyModule { }
And you still end up with only one instance:
Creating shared service: 683434