Dependency Injection
Essentially, when we ask for a construct instance, we want help constructing it. A DI system can act in one of two ways when asked to resolve an instance:
- Transient mode: The dependency is always created anew
- Singleton mode: The dependency is reused
Angular only creates singletons though which means every time we ask for a dependency it will only be created once and we will be given an already existing dependency if we are not the first construct to ask for that dependency.
The default behavior of any DI framework is to use the default constructor on a class and create an instance from a class. If that class has dependencies, then it has to resolve those first. Imagine we have the following case:
exportclassLogger { } exportclassService { constructor(logger:Logger) { } }
The DI framework would crawl the chain of dependencies, find the construct that does not have any dependencies, and instantiate that first. Then it would crawl upwards and finally resolve the construct you asked for. So with this code:
import { Service } from'./service'; exportclassExampleComponent { constructor(srv:Service) { } }
The DI framework would:
- Instantiate the logger first
- Instantiate the service second
- Instantiate the component third
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] }) exportclassFeatureModule{}
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.
Overriding an existing construct
There are cases when you want to override the default resolution of your construct. You can do so at the module level, but also at the component level. What you do is simply express which construct you are overriding and with which other construct. It looks like this:
@Component({ providers: [ { provide: Service, useClass : FakeService } ] })
The provide
is our known construct and useClass
is what it should point to instead. Let's imagine we implemented our Service
like so:
exportclassService { no:number = 0; constructor() {} }
And we added the following override to a component:
@Component({
providers: [{ provide : Service, useClass: FakeService }]
})
The FakeService
class has the following implementation:
exportclassFakeService { setno(value) { // do nothing } getno() { return99; } }
Now the component and all its view child components will always get FakeService
when asking for the Service construct.
Overriding at runtime
There is a way to decide what to inject for/into a construct at runtime. So far, we have been very explicit about when to override, but we can do this with a bit of logic added to it by using the useFactory
keyword. It works like the following:
let factory = () => { if(condition) { return new FakeService(); } else { return new Service(); } } @Component({ providers : [ { provide : Service, useFactory : factory } ] })
This factory can in itself have dependencies; we specify those dependencies with the deps
keyword like so:
let factory = (auth:AuthService, logger: Logger) => { if(condition) { return new FakeService(); } else { return new Service(); } } @Component({ providers : [ { provide : Service, useFactory : factory, deps: [AuthService, Logger] } ] })
Here, we highlighted the condition
variable, which is a Boolean. There can be a ton of reasons why we would want to be able to switch the implementation. One good case is when the endpoint don't exist yet and we want to ensure it calls our FakeService
instead. Another reason could be that we are in testing mode and by just changing this one variable we can make all our services rely on a fake version of themselves.
Overriding constants
Not everything, though, is a class that needs to be resolved; sometimes it is a constant. For those cases, instead of using useClass
, we can use useValue
, like so:
providers: [ { provide: 'a-string-token', useValue: 12345678 } ]
This is not really a class type, so you can't write this in a constructor:
constructor(a-string-token) . // will not compile
That wouldn't compile. What we can do instead is to use the @Inject
decorator in the following way:
constructor( @Inject('a-string-token') token) // token will have value 12345678
The useValue
is no different from useClass
when it comes to how to override it. The difference is of course that we need to type useValue
in our instruction to override rather than useClass
.
Resolving your dependencies with @Injectable
We took a little deep dive into DI in the previous section, but almost forgot about a very important decorator, @Injectable
. @Injectable
is not strictly mandatory to use for services in general. However, if that service has dependencies, then it needs to be used. Failure to decorate a service with @Injectable
that has dependencies leads to an error where the compiler complains that it doesn't know how to construct the mentioned service. Let's look at a case where we need to use the @Injectable
decorator:
import { Injectable } from '@angular/core'; @Injectable() export class Service { constructor(logger:Logger) {} }
In this case, Angular's DI machinery will look up Logger
and inject it into the Service
constructor. So, providing we have done this:
providers: [Service, Logger]
In a component or module, it should work. Remember, when in doubt, add @Injectable
to your service if it has dependencies in the constructor or will have in the near future. If your service lacks the @Injectable
keyword and you try to inject it into a component's constructor, then it will throw an error and your component will not be created.
This section set out to explain how DI works from a general standpoint and how it works in Angular. For the latter, it covered how to register constructs to work with Angular's DI machinery, but also how to override it. It is clear that the DI machinery is quite sophisticated. It can be scoped to the application level, by adding constructs to the providers array of Angular modules, but also to the component level and its view children. The main reason for describing the DI machinery was to teach you the possibilities of it, so you know how to best use it to your advantage when you define the architecture of your app.