ProvidedIn FeatureModule Is A Confusing Concept For Me In Angular 6.1.9
CAUTION: I don't really have a good grasp on Angular Modules. Or, on how modules related to the dependency-injector hierarchy, as is clearly demonstrated in this blog post. This post is really just for my own codification and an attempt to build a better mental model. Feel free to ignore.
TL;DR: Just use "providedIn: root".
In the release of Angular 6, the @Injectable() meta-data was updated to accept a "providedIn" property that associates a Service with an Angular module. According to the documentation, this is essentially the same as using the @NgModule() "providers" property; except, the "providedIn" approach is preferred and is supposed to make the various classes more tree-shakable. As such, I went through an application and attempted to move my feature-module services from the @NgModule() "providers" collection into the @Injectable() "providedIn: module" meta-data. This worked if the feature module only provided one service. But, if the feature module provided more than one service, the Dependency Injector could not locate most of the services at runtime, resulting in the following error (truncated):
Error: StaticInjectorError(AppModule) - NullInjectorError: No provider
This is easy enough to demonstrate. First, I created a SubModule that contained three routable components:
// Import the core angular services.
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";
// Import the application components and services.
import { SubComponent } from "./sub.component";
import { SubAComponent } from "./sub-a.component";
import { SubBComponent } from "./sub-b.component";
import { SubCComponent } from "./sub-c.component";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export var subModuleRoutes: Routes = [
{
path: "sub",
component: SubComponent,
children: [
{
path: "a",
component: SubAComponent
},
{
path: "b",
component: SubBComponent
},
{
path: "c",
component: SubCComponent
}
]
}
];
@NgModule({
imports: [
CommonModule,
RouterModule
],
declarations: [
SubComponent,
SubAComponent,
SubBComponent,
SubCComponent
]
})
export class SubModule {
// ...
}
As you can see, we have paths "a", "b", and "c" that map to SubAComponent, SubBComponent, and SubCComponent respectively. Each of these components is copy-pasted with only minor changes. Here's an example of one of the components:
// Import the core angular services.
import { Component } from "@angular/core";
// Import the application components and services.
import { ThingAService } from "./thing-a.service";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "sub-a",
template:
`
Sub-A Component is here!
`
})
export class SubAComponent {
constructor( thingAService: ThingAService ) {
console.log( "Thing A Service:", thingAService );
}
}
As you can see, SubAComponent injects a service called ThingAService. As you can guess, each of the components injects its own corresponding service: SubBComponent injects ThingBService and SubCComponent injects ThingCService. Each of these services is copy-pasted with only minor changes. Here's an example of one of the services:
// Import the core angular services.
import { Injectable } from "@angular/core";
// Import the application components and services.
import { SubModule } from "./sub.module";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// The problem becomes obvious when we log the SubModule that we're using in the meta-
// data. It will be DEFINED in only ONE of the services that is bootstrapped. Then, it
// will be UNDEFINED in the rest of the services.
console.log( "Bootstrapping A:", SubModule );
// NOTE: By using the "providedIn: Module" syntax, we are supposed to be able to get
// better tree-shaking ability in our Angular application. However, this does not seem
// to work very intuitively.
@Injectable({
providedIn: SubModule
})
export class ThingAService {
public label: string = "Thing A";
}
As you can see, in the @Injectable() meta-data, I'm asking Angular to provide this service in the SubModule feature module. According to the documentation, this is essentially the same as providing it in the @NgModules() "providers" collection.
Take note of the fact that I'm console-logging the SubModule reference that's being defined in the meta-data. This will become key in understanding why this approach doesn't work (for reasons I don't fully comprehend).
I then import this SubModule into the AppModule:
// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";
// Import the application components and services.
import { AppComponent } from "./app.component";
import { SubModule } from "./sub.module";
import { subModuleRoutes } from "./sub.module";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@NgModule({
imports: [
BrowserModule,
SubModule, // <---- Import the feature module.
RouterModule.forRoot(
[
...subModuleRoutes
],
{
// Tell the router to use the HashLocationStrategy.
useHash: true
}
)
],
declarations: [
AppComponent
],
bootstrap: [
AppComponent
]
})
export class AppModule {
// ...
}
Now, if I load this Angular application in the browser, we get the following output:
As you can see, when the Angular application is being bootstrapped, the SubModule reference is Undefined for all but one of the feature module services. As such, two of the three services don't get associated with the proper injector. In this case, the ThingAService is properly defined, which means we can navigate to SubAComponent:
But, if we try to navigate to SubBComponent or SubCComponent, we get an Angular provider error:
As you can see, because our SubModule references were undefined at bootstrap time, ThingBService and ThingCService were never associated with the correct injector. As such, the Angular DI container doesn't know where to get the services that it needs to inject into the SubBComponent and SubCComponent classes.
Now, according to this Angular Issue, the fact that the SubModule references are undefined has something to do with circular references. But, to me, this doesn't ring true since the SubModule references aren't undefined in all of the services - only in the majority of services. If this were truly a circular reference issue, then wouldn't it be the same for all three services? They are, after all, duplicates of each other's logic.
Right now, the SubModule feature module is being statically loaded at bootstrap time. But, we can fix our dependency-injection issue by turning the SubModule code into a lazy-loaded feature module:
// Import the core angular services.
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";
// Import the application components and services.
import { SubComponent } from "./sub.component";
import { SubAComponent } from "./sub-a.component";
import { SubBComponent } from "./sub-b.component";
import { SubCComponent } from "./sub-c.component";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
export var subModuleRoutes: Routes = [
{
path: "sub",
loadChildren: "./sub.module#SubModule" // <---- Layz loading the module.
}
];
@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{
// NOTE: Since this module is being lazy-loaded, the root segment has
// already been defined (as part of the lazy-load configuration). As
// such, the root segment here is empty.
path: "",
component: SubComponent,
children: [
{
path: "a",
component: SubAComponent
},
{
path: "b",
component: SubBComponent
},
{
path: "c",
component: SubCComponent
}
]
}
])
],
declarations: [
SubComponent,
SubAComponent,
SubBComponent,
SubCComponent
]
})
export class SubModule {
// ...
}
NOTE: I am also commenting-out the SubModule references in the AppModule, but I am not showing it.
Now, if we load the application in the browser and navigate to the SubModule, we can see that all of the SubModule references, in each of our services, is properly defined:
As you can see, once the feature module is lazy-loaded, the "SubModule" references in our services become defined. This fixes the "providedIn: SubModule" configuration and allows Angular's dependency-injector to locate the services needed for both SubBComponent and SubCComponent (which were formerly breaking).
Of course, the fact that I have to use different "providedIn" syntax for lazy-loaded vs. statically-loaded modules is frustrating. Luckily, I came across another Angular Issue comment by Trotyl Yu:
There are cases where user doesn't care about some service can only be used in specific lazy module, but just want it to be as lazy as possible, that will be:
- If a service only used in eager modules, let it bundled in eager bundle;
- If a service only used in lazy modules, let it bundled in lazy bundle;
- If a service used in both eager and lazy modules, let it bundled with eager bundle;
- If used nowhere, not bundle it.
That's when providedIn: 'root' should being used.
.... The value of providedIn has nothing to do with loading/bundling, only determines whether it is ALLOWED to use after being loaded.
And providedIn: 'root' means allowed anywhere.
So, it seems that using "providedIn: 'root'" may actually be the answer. And, in fact, if I go back and change all of my SubModule services to use "root" in the @Injectable() meta-data, things continue to work:
// Import the core angular services.
import { Injectable } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Injectable({
providedIn: "root"
})
export class ThingAService {
public label: string = "Thing A";
}
I made the same changes to ThingBService and ThingCService. And now, when I load the Angular app in the browser, even with the lazy-loading, the "providedIn: root" appears to work swimmingly:
This approach continues to work even if I switch from lazy-loading the SubModule back to statically-loading the SubModule.
So, it seems the key take-away here is just to always use:
providedIn: "root"
... for @Injectable() meta-data, even in feature-modules and even when the feature-module is lazy-loaded.
Which means, I have no idea when you would want to use the "providedIn: Module" syntax. Which is clearly because I have no idea what it does. Or how it relates to the DI hierarchy. But, maybe that won't matter. I'll just stick to using "root" for now until is causes an issue. Then, at that point, I can clarify my mental model.
Want to use code from this post? Check out the license.
Reader Comments
@All,
Here is the final version of the code (with lazy-loaded feature module and
providedIn: "root"
:https://github.com/bennadel/JavaScript-Demos/tree/master/demos/provided-in-fail-angular6
I think I ran across a comment somewhere a few weeks ago (can't remember where), that the { providedIn: FeatureModule } structure really only makes sense when creating and bundling independent modules, like libraries. I guess that makes sense, but I also ended up just setting all my references back to 'root'.
@Jason,
Ha, that's exactly what I did the moment I woke up this morning :) I did an extended RegEx search in my app:
... then went in and put "root" for all the things.
I think the comment about libraries is in one of the GitHub Issues. I remember seeing it as well when I was trying to figure out why this was breaking.