Dependency Injection
Last updated
Last updated
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.
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.
providedIn
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.
You can provide services at @Component
level by using the providers: []
property in the component decorator.
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
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.
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.
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.
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)
ApplicationConfig
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.
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.
Angular has two injector hierarchies:
EnvironmentInjector
Can be configured in one of two ways:
The @Injectable({ providedIn: root | plataform })
property to refer to root
or plataform
.
The ApplicationConfig
providers
array.
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
Angular creates ElementInjector
hierarchies implicitly for each DOM element.
Providing a service in the @Component
using providers: []
or viewProviders
property configures an ElementInjector
.
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()
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.
@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.
@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
.
@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.
Although there are specific places that have injection context, you can also create an injection context with .