Sunday, June 25, 2017

Dirty secrets on dependency injection and Angular - part 2

In the previous post "Dirty secrets on dependency injection and Angular - part 1", you've explored how DI at component level, can produce different instances of a service. Then you've experienced DI at module level. Once a service is declared using one token in the AppModule, the same instance is shared across all the modules and components of the app.

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 the HeroSearchComponent and HeroDetailComponent
  • HeroesModule to contain the HeroesComponent


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!

Friday, June 16, 2017

Dirty secrets on dependency injection and Angular - part 1

Let's talk about Dependency Injection (DI) in Angular. I'd like to take a different approach and tell you the stuff that surprise me when I've first learned them using Angular on larger apps...

Key feature from Angular even since AngularJS (ie: Angular 1.X), DI is a pure treasure from Angular, but injector hierarchy can be difficult to grasp at first. Add routing and dynamic load of modules and all could go wild... Services get created multiple times and if stateful (yes functional lovers, you sometimes need states) the global states (even worse 😅) is out of sync in some parts of your app.
To get back in control of the singleton instances created for your app singleton, you need to be aware of a few things.

Let's get started...

Tour of hero app


Let's reuse the tour of heroes app that you should be familiar with from when you first started at angular.io. Thansk to LarsKumbier for adapting it to webpack, I've forked the repo and adjust it to my demo's needs. All source code could be find on github.

In this version of Tour of heroes, the app displays a Dashboard page and a Heroes page. I've added a RecentHeroCompoent that displays the recently selected heroes in both pages. This component uses the ContextService to store the recently added heroes.


See AppModule in master branch.

Provider at Component level


Let's go to HeroSearchComponent in src/app/hero-search/hero-search.component.ts file and change the @Component decorator:
@Component({
  selector: 'hero-search',
  templateUrl: './hero-search.component.html',
  styleUrls: ['./hero-search.component.css'],
  providers: [ContextService] // [1]
})
export class HeroSearchComponent implements OnInit {

if you add line [1], you get something like this drawing:



Run the app again.
What do you observe?
The heroes page is working fine listing below the recently visited heroes. However going to Dashboard/SearchHeroComponent, the recently visited heroes list is empty!!

The recently added heroes is empty in HeroSeachComponent because you've got a different instance of ServiceContext. Dependency injection in Angular relies on hierarchical injectors that are linked to the tree of components. This means that you can configure providers at different levels:
  • for the whole application when bootstrapping it in the AppModule. All services defined in providers will share the same instance.
  • for a specific component and its sub components. Same as before but for à specific component. so if you redefine providers at Component level, you got a different instance. You've overriden global AppModule providers.

Tip: don't have app-scoped services defined at component level. Very rare use-cases where you actually want


Provider at Module level


What about providers at module level, if we do something like:



Let's first refactor the code, to introduce a SharedModule as defined in angular.io guide. In your SharedModule, we put the SpinnerComponent, the RecentHeroComponent and the ContextService. Creating the SharedModule, you can clean up the imports for AppModule which now looks like:

@NgModule({
  declarations: [
    AppComponent,
    HeroDetailComponent,
    HeroesComponent,
    DashboardComponent,
    HeroSearchComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    SharedModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService),
    AppRoutingModule
  ],
  providers: [
    HeroSearchService,
    HeroService,
    ContextService
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule {}

Full source code in github here. Notice RecentHeroComponent and SpinnerComponent has been removed from declarations. Intentionally the ContextService appears twice at SharedModule and AppModule level. Are we going to have duplicate instances?

Nope.
A Module does not have a specific injector (as opposed to Component which gets their own injector). Therefore when AppModule provides a service for token ContextService and imports a SharedModule that also provides a service for token ContextService, then AppModule's service definition "wins". This is clearly stated in AppModule angular.io FAQ.

Where to go from there


In this blog post you've seen how providers on component plays an important role on how singleton get created. Modules are a different story, they do not provide encapsulation as component.
Next blog posts, you will see how DI and dynamically loaded modules plays together. Stay tuned.