Hello everyone👋 My name is Danila, Angular front-end developer. I started learning Angular not so long ago, so I often come across complex topics that are incomprehensible and need to be analyzed. One of these topics was Dependency Injection. Well, let's figure it out :)
A few words about DI
Dependency injection, or DI, is a design pattern and mechanism for reusing code across different parts of an application.
A simple example is tea with sugar. We have tea, but without sugar it is not as tasty as with it. To get a tasty drink, we take a teaspoon (injector), add sugar (addiction) from the sugar bowl (provider) and put it in a mug.
Services
The most common dependency in Angular is services, but there can be other meanings as well.
A service in Angular is created like this:
@Injectable({ // Decorator that tells DI that this class can be injected
providedIn: 'root'
})
class Service {}
The provideIn property specifies where exactly to inject. Its available values:
-
root
- to the root upon request -
platform
- allows you to use the service in two or more root components (microservices, widgets) -
any
- in each area of implementation (that is, in regular modules, the instance will be common, but in lazy modules, each will have its own. P.S. will be removed in version 17)
Let's imagine that we have 10 services, but we only use 2 on the page. Then, thanks to dependency injection, we will not call 8 unused services. This will reduce the package size and resource consumption.
Injectors need providers
Injectors look for dependencies, and providers provide them.
The easiest way to specify a provider:
providers: [Service] // where, Service is a dependency
This entry can be interpreted as:
providers: [{ provide: Service, useClass: Service }]
// where, provide (token) is the key to search for the dependency,
// and useClass - creates an instance of the class when implemented
And now we implement
Well, DI is sorted out. What's next? Dependency injection uses Injector and its hierarchies. Let's look at his work:
Angular has 2 types of teaspoon injectors: ModuleInjector
(created explicitly and used in modules and @Injectable()
) and ElementInjector
(created implicitly in every DOM element with an empty value, configured in component and directive providers).
When you create an ElementInjector
in the directive/component constructor, it appears in the lexical environment and when further requested, Angular will look for it from your current module (for example, a page), going up the hierarchy until it reaches the highest one - @NullInjector()
, which always throws an error (unless you use the @Optional()
modifier).
Modifiers
When we create a component/directive, we get the dependencies in the class constructor. Thanks to modifiers, you can determine the visibility or way of creating instances:
constructor(@Optional() service: Service) {}
There are 4 types:
-
@Optional()
- optional dependency (used when it is not known whether there will be a dependency or not) -
@Self()
- looks for a dependency locally in a module -
@SkipSelf()
- searches for dependency starting from the parent element (scope) -
@Host()
- indicates that if the DI mechanism has reached the host, then the dependency search should be stopped
If necessary, they can be mixed. But not all, you cannot mix @Host()
and @Self()
, as well as @SkipSelf()
and @Self()
.
Properties use*
There are 4 methods:
-
useClass
- as I wrote earlier, creates an instance of a class -
useExisting
- unlike its predecessor, uses an already created instance (useful for singletons, when one instance is used throughout the application) -
useFactory
- uses a function to inject a dependency (convenient for overriding logic, for example, passing a service as function arguments, obtaining information from the service and using it in the constructor) -
useValue
- allows you to use any value as a dependency
What if we implement something other than a class?
For such cases there is InjectionToken. It can be used, for example, to transfer config in an application.
It is created like this:
import { InjectionToken } from '@angular/core';
const drink = new InjectionToken<string>('Drink')
And it is used like this:
providers: [{ provide: drink, useValue: 'Tea' }]
What if you need an array of dependencies?
If an array is required instead of one dependency, then the multi property is used. Then the values will not be overwritten, but the values will be added to the array.
providers: [
{ provide: drink, useValue: 'Tea', multi: true },
{ provide: drink, useValue: 'Water', multi: true },
]
export class drinkComponent implements OnInit {
constructor(arr: drink) {}
ngOnInit() {
console.log(this.arr); // ['Tea', 'Water']
}
}
I hope this article will help you better understand how dependency injection works :)
And, of course, good luck to everyone in learning such a beast as Angular😉
Top comments (0)