Dependency Injection-Behind the Scenes
Hello, awesome people hope you are doing awesome. In this blog, we will try to understand how dependency injection works behind the scenes.
In angular, we create components, pipes, directives, and all. Now to configure the dependencies that they need, angular provides us functionality called Dependency Injection.
Now there are two things called dependency provider and dependency consumer. The interaction between them is made possible by the abstract called an injector.
Okay, so let's take an example,
Adding an Injectable decorator to a class shows that it is available for injection and can be injected.
@Injectable()
class OneService {}
Now OneService is acting as a dependency that can be injected at multiple places.
Where it can be injected?
- At component level
- At the App Module level
ps: we will look at the Module specifically ahead with lazy loading
At the component level, we can simply add providers in the component decorator
@Component({
selector: 'app-list',
template: '...',
providers: [oneService]
})
class TestComponent {}
At the App module level, we provide it in NgModule decorator in providers similar to component one.
@NgModule({
selector: 'app-list',
template: '...',
providers: [oneService]
})
class TestComponent {}
Also from the component, we can directly add the dependency at the root level i.e. In-App Module using provideIn:’root’
Now generally it is most commonly used.
@Injectable({
providedIn: 'root'
})
class TestService {}
Once it is added to providers we can utilize it and inject it into elements that require it.
Now the most common way to inject a dependency is to simply add it in the constructor of that respective component where we need to utilize that dependency.
constructor(private loggerService:LoggerService) { }
Now I mentioned injector abstract manages the interaction between consumers and providers.
Let’s see how 👀
Instance Reutilization
We added dependency in the constructor to utilize it in a component. So when the component instantiates, angular checks whether the instance of the requested dependency already exists or not. If it exists then it continues with that and if not then the injector creates the instance for the same.
ps: To check the availability of instances it follows bottom-up approach of resolution you will understand it ahead.
Instance with respect to defined Level
Now let's discuss one more thing about the instance. Let's take an example the dependency is injected at the component level.
Suppose, we have Two components and they both require the same dependency and we have provided that at each respective component level.
So they both will have different instances created respectively.
And when we provide it in the root i.e. app module the instance created for the dependency remains the same throughout the application.
Now one point to note is why we define it in the app module and not the app component.
Because as per angular module hierarchy app module sits on the top and not the app component.
Side Note: Above App Module too we have a Module injector configured by the Platform module and above that, we have a Null Injector where everything ends. For more, you can refer to official docs.
SCOPE OF PROVIDER
Now how we can limit the scope of PROVIDER?
In the eagerly loading module, the injector in the root makes all the mentioned providers available at the start of the application.
Now suppose on a particular route we lazy load the module and it demands the respective dependencies which are listed in its provider's array i.e. module specific providers. But now the root component doesn’t know about it.
So for the lazy loaded module, the child injector gets populated with respect to the root injector and full fill the requirement of services mentioned in the provider's array of that module.
The lazy loaded module created has its own instance.
It can be implemented as follows:
import { Injectable } from ‘@angular/core’;@Injectable({
providedIn: ‘any’,
})
export class SomeService {}
Remember we discussed injecting services at the component level. It is termed as limiting the scope of providers at the Component Level.
But now you may ask If I have injected the same dependency(service) at both the app module level and component level then How angular knows which instance to use?
So we have a set of resolution rules that angular follows to avoid any type of conflict.
It resolves it in two phases:
- ModuleInjector — The ModuleInjector can be configured in one of two ways by using:
1. The @Injectable() providedIn property
2. The @NgModule() providers array - ElementInjector — Providing service in the @Component() decorator using its providers or viewProviders property configures an ElementInjector
When a component declares a dependency, Firstly — Angular tries to satisfy that dependency with its own ElementInjector. If it is not resolved then, it passes the request up to its parent component’s ElementInjector until Angular finds an injector that can handle the request or runs out of ancestor ElementInjector hierarchies.
Basically, it follows the bottom-up approach for resolution.
If Angular doesn’t find the provider in any ElementInjector hierarchies ie. in component-level providers, it goes back to the element where the request originated and looks in the ModuleInjector hierarchy. If Angular still doesn’t find the provider, it throws an error.
Conclusion
Now you know many things about dependency injection like how you can define it, where you can import it, how actually angular resolves dependencies, and many more things.
In case of any doubt or if you just want to say Hi! feel free to reach me on LinkedIn or GitHub.
If you like the blog make sure to take a look at my YouTube channel for more amazing stuff.