A deep dive into @Injectable and “providedIn” in Ivy
AngularInDepth is moving away from Medium. This article, its updates and more recent articles are hosted on the new platform inDepth.dev
In this article I want to explain how Angular @Injectable
decorator works under the hood and how providedIn
property is handled by Angular Ivy.
Introduction
Dependency injection is one of the most powerful core features of Angular. It has been there since the beginning in AngularJS and with the arrival of the new Ivy renderer engine it is time again to visit the internals of Dependency Injection. How does it work? What things got improved in @Injectable
creation and the resolution mechanism? Read on and take a deep dive into this interesting topic.
Pre-conditions
There a several ways how Angular (v9+) can work with your source code and can use different compilers and methods for it:
- ViewEngine + JIT
- ViewEngine + AOT
- Ivy + JIT
- Ivy + AOT
I will describe the newest and coolest way: Ivy + AOT with Angular CLI generated application.
Why does Angular need the Injectable decorator?
The @Injectable
decorator is used as an annotation tool only. This means that the decorator just contains important information for the compiler and will be removed at runtime and never will be called as a function.
What kinds of problems can be solved using providedIn?
The providedIn
property of the Injectable
decorator solves the following problems:
- Makes your providers tree-shakable
- Prevents duplication of provider instances
- Forces your providers to always create new instances
Knowledge of the internals of @Injectable
and providedIn
can help you to debug your providers, manage the count of instances, improve your bundle size and decide which injector to use.
The processing of Angular Injectables is separated to a build phase and a runtime phase.
The process is simple: during the build phase the compiler collects meta information about your Injectables, updates the source code with this data, and then uses it at runtime.
Compilation: in short
This is a simplified diagram of Angular application compilation process:
In this article we will first take a look at the AngularCompilerPlugin
, then the TSProgram
part, and finally the runtime
part.
The cases
Earlier, with View Engine we have been able to create and use Eager and Lazy modules, but Ivy opened new ways to use Angular components without modules (without module level injectors, aka “limp” injection), so the cases for today will be:
- Injectables and
providedIn
in eager and lazy modules - Injectables and
providedIn
in components and directives - Injectables and
providedIn
with lazy components without modules
Build phase
The main goal for AngularCompilerPlugin
in the context of providers is ещ:
- Collect classes marked with
@Injectable
decorators with theprovidedIn
property in your source code - Collect all providers from
@NgModule
,@Component
and@Directive
provider lists in your source code - Add meta information (I will explain below what exactly this information is) to all of the collected classes
- Remove the
@Injectable
decorator from classes
When you type ng serve
to your terminal Angular starts webpack compilation and AngularCompilerPlugin
instance will be created. Then AngularCompilerPlugin
calls internal method _make
which will create instance of TSProgram
.
TSProgram
then starts to handle and transform each item emitted by the AngularCompilerPlugin
file:
As you can see Visitor
pattern is used here (the most popular in AST-related tools, like webpack
, AngularCompilerPlugin
, eslint
, prettier
, etc) to walk through and process each file.
Angular will find all injectables by traversing through the “files” and “includes” properties of your tsconfig.json
.
To achieve our goal we need only AST-expressions, which are classes decorated as Injectable
.
After Visitor
finds a class with Injectable
decorator, it should handle its data and add the corresponding annotations to the class (type
in Angular words).
To summarize @Injectable
compilation is as follows:
This process is not applied to the InjectionTokens
, it will not be annotated.
The most interesting functions for us in this process are InjectableDecoratorHandler.compile and compileInjectable.
compileInjectable function handle all types of providers: classes with Injectable
decorator with providedIn
property and common classes with Injectable
decorator passed to the @NgModule.providers
or @Component.providers
list. All modes of providers configuration are supported here: useClass
, useFactory
, useValue
, etc (yes, @Injectable
decorator can be configured using e.g. useFactory
).
The main goal of these functions is to calculate AST-subtree for typescript to add some static properties (annotations) for your injectable’s class. These properties are:
ɵprov
- property withInjectableDef
type, contains the injectable's informationɵfac
- property containing factory for new injectable instances creation
But why Angular needs these properties at all?
They are needed, because:
ɵfac
— just to be used as part ofɵprov
.ɵprov
— Angular needs to know in runtime which exactly Factory (ɵprov.factory
=ɵfac
) should be used to create an instance when you want to inject some Type (ɵprov.token
) and which Injector should store instance of this injectable (ɵprov.providedIn
).
Since InjectionTokens
are not processed by the compiler, property will be created for it in the runtime.
Here is how results of InjectableDecoratorHandler.compile (AST-subtree) looks like based on input meta for simple eager Injectable called ApplicationService
:
Then Angular will update your source code with calculated expressions:
Build phase for eager, lazy injectables (provided in lazy modules or without modules) is the same. Yes, Angular finds your lazy routes and lazy components and pre-compiles definitions for it, but injectables compilation goes through the same way. Also it works for @Component.providers
and @Directive.providers
.
And that’s all for the build phase. These annotation properties will be used in runtime to create and identify our injectables. When you serve your application, you can see in browser’s source tab that your source code changed already:
Runtime phase
Injectables will be instantiated when you need (inject) it in some entity, like component or directive. In our example, ApplicationService
will be created as a part of AppComponent
creation.
To inject something Ivy Engine uses two functions under the hood: ɵɵdirectiveInject and ɵɵinject.
From the docs: ɵɵdirectiveInject is intended to be used for directive, component and pipe factories. All other injection use ɵɵinject which does not walk the NodeInjector
tree.
ApplicationService
will be resolved from ApplicationComponent
because Angular marked that is should be injected by ɵɵdirectiveInject
already:
So here our injectable instantiation begins. First, take a look at a whole path:
This process is the same for @Injectable
and InjectionToken
.
As you can see, Ivy Injectors enter the game here. Please check these articles about Ivy DI and injectors called NodeInjector and R3Injector because it is great area of knowledge:
- Angular DI: Getting to know the Ivy NodeInjector by Alexey Zuev
- Asynchronous Modules and Components in Angular Ivy by Artur Androsovych
In a few words NodeInjector
is used for components, directives and it's providers and works as component-level injector. But R3Injector
is used as a module-level injector and has records
property that keeps instances of injectables.
Now we are going to understand the core functions of Ivy injectables runtime:
- getOrCreateInjectable
- injectRootLimpMode
- R3injector.get and injectableDefInScope
- searchTokensOnInjector and getNodeInjectable
In getOrCreateInjectable firstly we search our injectable through component-level injectors using bloom filters perfectly described by Max Koretskyi at NgConnect and Alexey Zuev in his article. Then if not succeeded we try to find injectable on the current module level injector.
Resolving component-level injectables
searchTokensOnInjector and getNodeInjectable methods will search your injectable on NodeInjector
, particularly on the TView
and LView
and create it, if it doesn't exists already.
TView
stores your injectable's Type and LView
on first pass stores Factory and then change it to the injectable's instance.
So all other times when you will try to get component-level injectable you will always get it from it’s LView
.
Resolving “limp” injectables
In Ivy, if there is no module injector, we still can inject our injectable:
This mode has some restrictions — only injectables with providedIn
set to root
can be injected this way.
The token and it’s instance is stored in the InjectableDef
- property.
If you will not set providedIn
to root
, you will get this error in limp mode:
How can we create a component (and it’s injectables) in limp mode using high level Angular API? I think you saw this code already if you are interested in the Ivy Engine:
Resolving module-level injectables
Now take a look at a common way of injectables resolution, when we have module-level injector. Firstly R3injector.get checks that input data is the injectable token and R3injector.records does not already have this token. Then it registers it and creates an instance using R3Injector.hydrate method:
Token and it’s instance registered in R3injector.records property.
There is a heart of the providedIn
property - injectableDefInScope function. It checks that the compiled injectable definition can be found and it's scoped to this module-level injector. Angular uses R3injector.get method while traversing module-level injectors, so your injectable should be provided at least in one. Otherwise you will get an error.
So, this function will return true (and then the instance will be created), when:
- you set
providedIn
toany
(always!) - you set
providedIn
toroot
and now you are in the root module injector context - you set
providedIn
toSomeModule
andthis.injectorDefTypes
contains this module
Finally, we have come to the end.
Resources
The code can be found here.
If you are interested in solving business problems understanding and using Angular internals, take a look at this articles also:
- How to avoid angular injectable instance duplication
- Requests tracking in Angular application with child module injectors without lazy loading
and follow me on twitter and medium.
Wizards and magicians
Big thanks to Max Koretskyi , creator of the indepth platform for help, review and inspiration.
I’m grateful AngularInDepth community for help and review:
Conclusion
Providers and Injectables are a huge part of Angular and Ivy. It’s compilation and resolution mechanism and work modes are not simple, but when you understand it completely you can become a master of Angular DI.
Today we explored how Angular handles injectables at the build and runtime phases, which types of injectable you can create, and which values of providedIn
you can use.
Here is a simple infographic to remember the values of the providedIn
property.
Thanks for reading!
Originally published at https://indepth.dev on March, 2020.