The Concept of Injector in Angular
An Injector
is a mechanism dedicated to dependency injection in Angular. Not all frameworks build their Dependency Injection (DI) the same way. For example, the Spring IoC Container can be mentioned for the famous Java framework. The Angular documentation has long emphasized providers
, leaving aside other elements necessary for DI. Injectors
are no exception, which is why I propose to see together how they work.
Until version 13, explanations on DI focus on use cases. It was only after that the internal workings were documented (some examples here and there).
How an Injector Works
Simply put, it is nothing more or less than an object that will manage providers
. It must provide dependencies and ensure that they remain singleton via an internal cache. This internal cache retains references to the providers
, which allows returning existing instances. If necessary, the Injector
takes care of instantiation and retains a reference for future calls (e.g., injection token of a class or service). Injectors
are therefore positioned between the providers
defined in the application and the components/services that consume these providers
.
What can be noticed is that the operation of an Injector
alone is fairly trivial. All the complexity comes from the fact that there are many of them in an Angular application and as an Angular developer, we do not manipulate them directly (very rarely).
Injectors, Injectors everywhere
Okay, they are many, but how many? Each NgModule
will automatically create an Injector
, and each component or directive that has providers
or viewProviders
will also create an Injector
. In the first case, it is a ModuleInjector
, and in the second case, we talk about an ElementInjector
. We therefore understand that the number of Injectors
will quickly grow with our application (at a minimum, as many Injectors
as NgModule
).
When an Injector
is destroyed, the instances it had in its cache are also destroyed. NgModules
are not often destroyed but components are a different story. So ElementInjectors
are frequently created and destroyed, as are the associated services.
Injector and Dependency Resolution
Each dependency is resolved by progressively climbing the Injector
chain. For a service injected at the component level, the ElementInjector
(if the component has providers
) is successively interrogated, the ModuleInjector
of the module that declares the component, then the Injector
of the parent module, the Injector
of the parent of the parent, etc., until reaching the root ModuleInjector
. Of course, the search stops as soon as an instance of the requested service is obtained.
At the top of the structure, it is the same operation for all Angular applications. There is a PlatformModule
that is called in main.ts
and everything that has not been found ends up in a NullInjector
(cf. illustration below).
The list of providers
for an Injector
is not set in stone. It is possible to assign other services to the root Injector
after the application starts. This is what happens when you do providedIn: "root"
on a service that is only used in a module with lazy-loading.
Injector, Bundle & Instanciation
It’s impossible to talk about Injector
without discussing bundle management and instantiation because confusion is common. Just because a service is providedIn: "root"
doesn’t mean it will be in the main bundle. Similarly, this service will not necessarily be instantiated at the start of the application.
As for the bundle, tree-shaking takes care of cleaning up and removing anything that is not necessary. So all services that are not used in the main bundle are excluded. However, be aware that services defined in the providers
of a module do not benefit from this advantage. They will be included in the bundle along with the module! And what about services providedIn: "root"
used in multiple modules with lazy-loading? That’s where the commonChunk option comes in, which will determine whether the service is duplicated in each bundle or if a common bundle is made (default value).
Regarding instantiation, it is postponed until the last moment. As long as there are no consumers of the service, there is no reason to instantiate it. This means that the providedIn: "root"
part only guarantees that the service will be a singleton available throughout the application. This same service may never be instantiated. So be careful with services because DI is magical but there are some gotchas.
Conclusion
I hope I have helped you gain a better understanding of the role of Injector
in Angular’s operation. You now know that it is an object that manages instantiations at the root, in modules, and in certain components or directives. These instantiations are shared with child elements but they can also be overridden locally by a submodule or in a component.
When you do dynamic component loading, you can define the Injector
used yourself (see ViewContainerRef
documentation). For the curious, I recommend checking out the impact of standalone components on Injectors
, more details here.
Go further : If you want a more details on Injector
and Injection in Angular, I recommend checking this deep dive : How Angular Dependency Injection works under the hood