Dependency Injection in Angular using providers

So far we have only discussed Dependency Injection in general, but Angular has some constructs, or decorators, to ensure that Dependency Injection does its job. First imagine a simple scenario, a service with no dependencies:

export class SimpleService {}

If a component exists that requires an instance of the service, like so:

@Component({
selector: 'component'
})
export class ExampleComponent {
constructor(srv: Service) {}
}

The Angular Dependency Injection system comes in and attempts to resolve it. Because the service has no dependencies, the solution is as simple as instantiating Service, and Angular does this for us. However, we need to tell Angular about this construct for the DI machinery to work. The thing that needs to know this is called a provider. Both Angular modules and components have access to a providers array that we can add the Service construct to. A word on this though. Since the arrival of Angular modules, the recommendation is to not use the providers array for components. The below paragraphs are merely there to inform you how providers for components work. 

This will ensure that a Service instance is being created and injected at the right place, when asked for. Let's tell an Angular module about a service construct:

import { Service } from "./Service";

@NgModule({
providers: [Service]
})
export class FeatureModule{}

This is usually enough to make it work. You can, however, register the Service construct with the component class instead. It looks identical:

@Component({
providers: [Service]
})
export ExampleComponent {}

This has a different effect though. You will tell the DI machinery about this construct and it will be able to resolve it. There is a limitation, however. It will only be able to resolve it for this component and all its view child components. Some may see this as a way of limiting what components can see what services and therefore see it as a feature. Let me explain that by showing when the DI machinery can figure out our provided service:

Everybody's parent – it works: Here, we can see that as long as the component highest up declares Service as a provider, all the following components are able to inject Service:

AppComponent // Service added here, Can resolve Service
TodosComponent // Can resolve Service
TodoComponent // Can resolve Service

Let's exemplify this with some code:

// example code on how DI for works for Component providers, there is no file for it
// app.component.ts
@Component({
providers: [Service] // < - provided,
template : `<todos></todos>`
})
export class AppComponent {}

// todos.component.ts
@Component({
template : `<todo></todo>`,
selector: 'todos'
})
export class TodosComponent {
// this works
constructor(private service: Service) {}
}

// todo.component.ts
@Component({
selector: 'todo',
template: `todo component `
})
export class TodoComponent {
// this works
constructor(private service: Service) {}
}

TodosComponent – will work for its children but not higher up: Here, we provide Service one level down, to TodosComponent. This makes Service available to the child components of TodosComponent but AppComponent, its parent, misses out:

AppComponent // Does not know about Service
TodosComponent // Service added here, Can resolve Service
TodoComponent // Can resolve Service

Let's try to show this in code:

// this is example code on how it works, there is no file for it
// app.component.ts
@Component({
selector: 'app',
template: `<todos></todos>`
})
export class AppComponent {
// does NOT work, only TodosComponent and below knows about Service
constructor(private service: Service) {}
}

// todos.component.ts
@Component({
selector: 'todos',
template: `<todo></todo>`
providers: [Service]
})
export class TodosComponent {
// this works
constructor(private service: Service) {}
}

// todo.component.ts
@Component({
selector: 'todo',
template: `a todo`
})
export class TodoComponent {
// this works
constructor(private service: Service) {}
}

We can see here that adding our Service to a component's providers array has limitations. Adding it to an Angular module is the sure way to ensure it can be resolved by all constructs residing inside of that array. This is not all though. Adding our Service to an Angular module's providers array ensures it is accessible throughout our entire application. How is that possible, you ask? It has to do with the module system itself. Imagine we have the following Angular modules in our application:

AppModule
SharedModule

For it to be possible to use our SharedModule, we need to import it into AppModule by adding it to the imports array of AppModule, like so:

//app.module.ts

@NgModule({
imports: [ SharedModule ],
providers: [ AppService ]
})
export class AppModule{}

We know this has the effect of pulling all constructs from the exports array in SharedModule, but this will also concatenate the providers array from SharedModule to that of AppModule. Imagine SharedModule looking something like this:

//shared.module.ts

@NgModule({
providers : [ SharedService ]
})
export class SharedModule {}

After the import has taken place, the combined providers array now contains:

  • AppService
  • SharedService

So the rule of thumb here is if you want to expose a service to your application, then put it in the Angular module's providers array. If you want to limit access to the service, then place it into a component's providers array. Then, you will ensure it can only be reached by that component and its view children.

Up next, let's talk about cases when you want to override the injection.