In this article, let's revisit DI in the context of lazy-loading modules. You'll see the feature modules dynamically loaded have a different behaviour.
Let's get started...
Tour of hero app
Let's reuse the tour of heroes app that you should be familiar with from our previous post. All source code could be find on github.
As a reminder, in our Tour of heroes, the app displays a Dashboard page and a Heroes page. We've added a
RecentHeroCompoent
that displays the recently selected heroes in both pages. This component uses the ContextService
to store the recently added heroes.
In the previous blog, you've worked your way to refactor the app and introduced a
SharedModule
that contains RecentHeroCompoent
and use the ContextService
. Let's refactor the app to break it into more feature modules:
DashboardModule
to contain theHeroSearchComponent
andHeroDetailComponent
HeroesModule
to contain theHeroesComponent
Features module
Here is a schema of what you have in the lazy.loading.routing.shared github branch:
DashboardModule
is as below:
@NgModule({
imports: [
CommonModule,
FormsModule,
DashboardRoutingModule, // [1]
HeroDetailModule,
SharedModule // [2]
],
declarations: [
DashboardComponent,
HeroSearchComponent
],
exports: [],
providers: [
HeroService,
HeroSearchService
]
})
export class DashboardModule { }
In [1] you define
DashboardRoutingModule
.
In [2] you import
SharedModule
which defines common components like SpinnerComponent
, RecentHeroesComponent
.
HeroModule
is as below:
@NgModule({
imports: [
CommonModule,
FormsModule,
HeroDetailModule,
SharedModule, // [1]
HeroesRoutingModule
],
declarations: [ HeroesComponent ],
exports: [
HeroesComponent,
HeroDetailComponent
],
providers: [ HeroService ] // [2]
})
export class HeroesModule { }
In [1] you import
SharedModule
which defines common components like SpinnerComponent
, RecentHeroesComponent
.
Note in [2] that
HeroService
is defined as provider in both modules. It could be a candidate to be provided by SharedModule. This service is stateless however. Having multiple instances won't bother us as much as a stateful service.
Last, let's look at
AppModule
:
@NgModule({
declarations: [ AppComponent ], // [1]
imports: [
BrowserModule,
FormsModule,
HttpModule,
SharedModule, // [2]
InMemoryWebApiModule.forRoot(InMemoryDataService),
AppRoutingModule // [3]
],
providers: [], // [4]
bootstrap: [ AppComponent ],
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {}
In [1], the
declarations
section is really lean as most components are declared either in the features module or in the shared module.
In [2], you now import the
SharedModule
form AppModule. SharedModule
is also imported in the feature modules. From our previous post we know, in statically loaded module the last declared token for a shared service wins. There is eventually only one instance defined. Is it the same for lazy-loading?
In [3] we defined the module for lazy loading, more in next section.
In [4],
providers
section is lean similar to declarations
as most providers are defined at module level.
Lazy loading modules
AppRoutingModule
is as below:
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadChildren: './dashboard/dashboard.module#DashboardModule' }, // [1]
{ path: 'detail/:id', loadChildren: './dashboard/dashboard.module#DashboardModule' },
{ path: 'heroes', loadChildren: './heroes/heroes.module#HeroesModule' }
]
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
In [1], you'll define lazy load
DashboardModule
with loadChildren
routing mechanism.
Running the app, you can observe the same syndrom as when we define
ContextService
at component level: DashboardModule
has a different instance of ContextService
than HeroesModule
. This is easily observable with 2 different lists of recently added heroes.
Checking angular.io module FAQ, you can get an explanation for that behaviour:
Angular adds@NgModule.providers
to the application root injector, unless the module is lazy loaded. For a lazy-loaded module, Angular creates a child injector and adds the module's providers to the child injector.
Why doesn't Angular add lazy-loaded providers to the app root injector as it does for eagerly loaded modules?
The answer is grounded in a fundamental characteristic of the Angular dependency-injection system. An injector can add providers until it's first used. Once an injector starts creating and delivering services, its provider list is frozen; no new providers are allowed.
What about if you what a singleton shared across all your app for
ContextService
? There is a way...
Recycle provider with forRoot
Similar to what
RouterModule
uses: forRoot.
Here is a schema of what you have in the lazy.loading.routing.forRoot github branch:
In
SharedModule
:
@NgModule({
imports: [
CommonModule
],
declarations: [
SpinnerComponent,
RecentHeroComponent
],
exports: [
SpinnerComponent,
RecentHeroComponent
],
//providers: [ContextService], // [1]
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
})
export class SharedModule {
static forRoot() { // [2]
return {
ngModule: SharedModule,
providers: [ ContextService ]
}
}
}
In [1] remove
ContextService
as a providers. Define in [2] a forRoot
method (the naming is an broadly accepted convention) that
returns a ModuleWithProviders interface. This interface define a Module with a given list of providers. SharedModule
will reuse defined ContextService
provider defined at AppModule
level.
In all feature modules, imports
SharedModule
.
In
AppModule
:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
//SharedModule, // [1]
SharedModule.forRoot(), // [2]
InMemoryWebApiModule.forRoot(InMemoryDataService),
AppRoutingModule
],
providers: [],
bootstrap: [
AppComponent
],
schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA]
})
export class AppModule {
}
In [1] and [2], replace the
SharedModule
imports by SharedModule.forRoot()
. You should only call forRoot at highest level ie: AppModule
level otherwise you will run in multiple instances.
To see the source code, take a look at lazy.loading.routing.forRoot github branch:
Where to go from there
In this blog post you've seen how
providers
on lazy-loaded modules behaves differently that in an app with eagerly loaded modules.
Dynamic routing brings its lot of complexity and can introduce difficult-to-track bugs in your app. Specially if you refactor from statically loaded modules to lazy loaded ones. Watch out your shared module specially if they provide services.
The Angular team even recommends to avoid providing services in shared modules. If you go that route, you still have the forRoot alternative.
Happy coding!
Tweet
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.