Dependency Injection
About
The two main roles of DI in Angular are:
Dependency consumer;
Dependency provider.
Angular facilitates the interaction between these two roles using an abstraction called Injector
.
When a dependency is requested, the Injector
checks its registry to see if there is an instance already available there.
If not, a new instance is created and stored in the registry.
Angular can create an application-wide injector known as root
injector, during the bootstrap process.
Providing a Dependency
The DI system relies internally on a runtime context where the current injector is available. This means that injectors can only work when code is executed in such context.
This means in pratice that you cannot call inject()
anywhere.
Knowing when you are in an injection context will allow you to use the inject()
.
Check the docs to see specific places that have injection context.
Although there are specific places that have injection context, you can also create an injection context with runInInjectionContext
.
At application root level using providedIn
providedIn
@Injectable({
providedIn: 'root'
})
Angular creates a single, shared instance of the service, and injects it into any class you ask for it.
Using providedIn: 'root'
when creating a service, allows injecting the service into all other classes.
You must not declare the service in @Component({ providers: [] })
array when using it, or it will create new instances.
It enables Angular code optimizers to effectively remove services that are unused.
Known as Tree-Shaking.
At component level
You can provide services at @Component
level by using the providers: []
property in the component decorator.
@Component({
providers: [SomeService]
})
The default behavior is for the injector to instantiate that class using new
operator.
In this case, the service becomes available to all instances of this component and other components and directives used in the Template.
Declaring services like this causes the services to always be included in your application - even if the service is unused.
No Tree-Shaking.
You can configure DI to associate a Service provider token
with a different class or other values like.
The expanded provider configuration is an object literal with two properties:
The
provide
property holds the token that serves as the key for consuming the dependency value.The second property is a provider definition object, which tells the injector how to create the dependency value. This can be one of the following:
useClass
useExisting
useFactory
useValue
useClass
useClass
[{ provide: Logger, useClass: TestLogger }]
Tells Angular DI to instanciate a provided class when a dependency is injected.
Useful to substitute/extend an alternative implementation for a common or default class.
Or even emulate the behavior of the real class in a test case.
If the alternative class providers have their own dependencies, specify both providers in the providers metadata property of the parent module or component.
[
UserService, // dependency needed in `EvenBetterLogger`.
{ provide: Logger, useClass: EvenBetterLogger }
]
useExisting
useExisting
Allows you to mapo one token to another. (Alias a token and reference any existing one)
In effect, the first token is an alias for the service associated with the second token, creating two ways to access the same service object.
[
NewLogger,
// Alias OldLogger with reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger},
]
useFactory
useFactory
Allows you to create a dependency object by calling a factory function. With this approach, you can create a dynamic value based on information available in the DI and elsewhere in the app.
Check the example in the docs.
useValue
useValue
Lets you associate a static value with a DI token.
Use this technique to provide runtime configuration constants such:
Website base addresses.
Feature flags.
Also use a value provider in a unit test to provide mock data in place of a production data service.
The following example show how to inject global data variables. (APP_DATA_TOKEN
and APP_DATA
don't have to be created in the same file)
import { InjectionToken } from '@angular/core';
interface AppData {
ip: string;
env: string;
...
}
// `InjectionToken` requires a parameter which is just a description for the Token
const APP_DATA_TOKEN = new InjectionToken<AppData>('app.data description');
const APP_DATA: AppData = { ip: '127.0.0.1', env: 'development' };
@Component({
providers: [{ provide: APP_DATA_TOKEN, useValue: APP_DATA }]
})
export class Component {
constructor(@Inject(APP_DATA_TOKEN) appData: AppData) {}
}
At application root level using ApplicationConfig
ApplicationConfig
export const appConfig: ApplicationConfig = {
providers: [{ provide: HeroService }],
};
The service will be available to all components, directives and pipes.
But the service will always be included in your application - even if the service is unused.
No Tree-Shaking.
Injectors have rules that you can leverage to achieve the desired visibility of injectables in your application.
There may be sections in your application that work completely independent, with its own local copies of the services and other dependencies that it needs.
With hierarchical DI, you can:
Isolate sections of the application and give them their own private dependencies.
Have parent components share certain dependencies with its child components only.
Resolution Rules (Sequence)
When a Component declares a dependency, Angular tries to satisfy the dependency with its own
ElementInjector
.If the components lacks the provider, it passes the request up to its parent
ElementInjector
.This request keeps forwarding up until Angular finds an injector that can handle the request or runs out of ancestors
ElementInjector
.If it still could not find it, it goes back to the element where the request originated and looks in the
EnvironmentInjector
hierarchy.If it still does not find it, it throws an error.
If you have registered a provider for the same DI token at different levels, the first one Angular encounters is the one it uses to resolve the dependency.
Injector Hierarchies
Angular has two injector hierarchies:
EnvironmentInjector
EnvironmentInjector
Can be configured in one of two ways:
The
@Injectable({ providedIn: root | plataform })
property to refer toroot
orplataform
.The
ApplicationConfig
providers
array.
Tree-Shaking
Only available with @Injectable({ providedIn })
method.
Is an optimization tool, which removes services that your application isn't using, resulting in smaller bundle sizes.
ElementInjector
ElementInjector
Angular creates ElementInjector
hierarchies implicitly for each DOM element.
Providing a service in the @Component
using providers: []
or viewProviders
property configures an ElementInjector
.
@Component({
providers: []
// Or
viewProviders: []
})
Services provided like this are available at that component instance, and may be visible at child component/directives based on visibility rules. (Components and directive on the same element share an injector)
When the component instance is destroy, so is that service instance.
Angular's resolution behavior can be modified.
Use them when injecting services, in:
Component class constructor.
Or the
inject()
configuration.
@Optional()
@Optional()
Allows Angular to consider a service you inject to be optional.
So if it cannot be resolved at runtime, Angular resolves the service as null
, rather than throwing an error.
export class Component {
constructor(@Optional() private logger?: LoggerService) {}
}
@Self()
@Self()
Use it so that Angular will only look at the ElementInjector
for the current component or directive. (It will not go up to it's ancestors)
A good case for @Self()
is to inject a service but only if it is available on the current host element.
In the following example, if LoggerService
is provided inside Component
class it will be availble, otherwise it will be null
, because it won't check if the parent component of Component
has it.
export class Component {
constructor(@Self() @Optional() private logger?: LoggerService) {}
}
@SkipSelf()
@SkipSelf()
It is the opposite of @Self()
, so Angular starts looking in the parent ElementInjector
, rather than in the current one.
@Self()
and @SkipSelf()
are exclusive from one another.
In the following example, any instance of LoggerService
provided inside Component
class will be ignored. Angular will try to get it from Component
parent, and if the parent don't provide it then it will be null
.
export class Component {
constructor(@SkipSelf() @Optional() private logger?: LoggerService) {}
}
@Host()
@Host()
Lets you designate a component as the last stop in the injector tree when searching for providers.
Even if there is a service instance further up the tree, Angular won't continue looking.
@Host()
and @Self()
are exclusive from one another.
Last updated