A deep dive into @Injectable and “providedIn” in Ivy

Nikita Balakirev
Angular In Depth
Published in
8 min readMar 1, 2020

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:

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:

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 the providedIn 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.

AngularCompilerPlugin starts compilation

TSProgram then starts to handle and transform each item emitted by the AngularCompilerPlugin file:

Stack trace showing the way from file emitting to file transformation

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:

Injectable compilation process

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 with InjectableDef 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:

  • ɵfacjust to be used as part of ɵprov.
  • ɵprovAngular 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:

Simple eager ApplicationService
Representation of AST-subtree for ApplicationsService annotations

Then Angular will update your source code with calculated expressions:

As a result of IvyVisitor.visitClassDeclaration method we will get annotated sources

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:

Updated sources of ApplicationService in browser

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:

ApplicationService creation starts here

So here our injectable instantiation begins. First, take a look at a whole path:

Ivy runtime injectables creation

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:

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:

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:

Limp mode injection by Ivy

This mode has some restrictions — only injectables with providedIn set to root can be injected this way.

injectRootLimpMode in action, instance of our injectable will be created here!

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:

Limp component creation

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 registration and instantiation using module injector

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.

The guy who handles providedIn

So, this function will return true (and then the instance will be created), when:

  • you set providedIn to any (always!)
  • you set providedIn to root and now you are in the root module injector context
  • you set providedIn to SomeModule and this.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:

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.

--

--

Nikita Balakirev
Angular In Depth

Writer at AngularInDepth and Indepth.dev, web enthusiast and lead frontend developer at Nexign.