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 an 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 the AngularCompilerPlugin in the context of providers is to:

  • 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 in your terminal, Angular starts Webpack compilation and the AngularCompilerPlugin instance is be created. Then the AngularCompilerPlugin calls the internal method _make which will create an instance of TSProgram.

compiler.hooks.make.tapPromise('angular-compiler', compilation => this._donePromise = this._make(compilation));
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, the 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.

The compileInjectable function handles all types of providers: classes  with the Injectable decorator with the providedIn property and common classes with Injectable decorator passed to the @NgModule.providers or @Component.providers list. All modes of provider 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 the 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 does Angular even need these properties?

They are needed, because:

  • ɵfac — just to be used as part of ɵprov.
  • ɵprovAngular needs to know at runtime exactly which 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 the instance of this injectable (ɵprov.providedIn).

Since InjectionTokens are not processed by the compiler, the ɵprov property will be created for it in the runtime.

Here is how the results of InjectableDecoratorHandler.compile (AST-subtree) look like based on input meta for simple eager Injectable called ApplicationService:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ApplicationService {
  constructor() {
    console.log('>>> ApplicationService constructor!');
  }

  call() {
    console.log('>>> ApplicationService call!');
  }
}
Simple eager ApplicationService
Representation of AST-subtree for ApplicationsService annotations

Then Angular will update your source code with calculated expressions:

// Replace the class declaration with an updated version.
      node = ts.updateClassDeclaration(
          node,
          // Remove the decorator which triggered this compilation, leaving the others alone.
          maybeFilterDecorator(node.decorators, this.compilation.decoratorsFor(node)),
          node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
          // Map over the class members and remove any Angular decorators from them.
          members.map(member => this._stripAngularDecorators(member)));
As a result of IvyVisitor.visitClassDeclaration method we will get annotated sources

The 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 the 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 the 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, the Ivy Injectors enter the game here. Please check these articles about Ivy DI and injectors called NodeInjector and R3Injector because it is a great area of knowledge:

In a few words NodeInjector is used for components, directives and its providers and works as a component-level injector. But R3Injector is used as a module-level injector and has a records property that keeps instances of injectables.

Now we are going to understand the core functions of Ivy injectables runtime:

First in getOrCreateInjectable we search our injectable through component-level injectors using bloom filters which were perfectly described by Max Koretskyi at NgConnect  and Alexey Zuev in his article. Then if not successful we try to find injectable on the current module level injector.

Resolving component-level injectables

The 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.

/**
* Retrieve or instantiate the injectable from the `LView` at particular `index`.
*
* This function checks to see if the value has already been instantiated and if so returns the
* cached `injectable`. Otherwise if it detects that the value is still a factory it
* instantiates the `injectable` and caches the value.
*/
export function getNodeInjectable(
    lView: LView, tView: TView, index: number, tNode: TDirectiveHostNode): any {
  let value = lView[index];
  const tData = tView.data;
  if (isFactory(value)) { <--- Here is first pass check
    const factory: NodeInjectorFactory = value;
    if (factory.resolving) {
      throw new Error(`Circular dep for ${stringifyForError(tData[index])}`);
    }
    const previousIncludeViewProviders = setIncludeViewProviders(factory.canSeeViewProviders);
    factory.resolving = true;
    let previousInjectImplementation;
    if (factory.injectImpl) {
      previousInjectImplementation = setInjectImplementation(factory.injectImpl);
    }
    enterDI(lView, tNode);
    try {
      value = lView[index] = factory.factory(undefined, tData, lView, tNode); <--- And here is instance creation
      // This code path is hit for both directives and providers.
      // For perf reasons, we want to avoid searching for hooks on providers.
      // It does no harm to try (the hooks just won't exist), but the extra
      // checks are unnecessary and this is a hot path. So we check to see
      // if the index of the dependency is in the directive range for this
      // tNode. If it's not, we know it's a provider and skip hook registration.
      if (tView.firstCreatePass && index >= tNode.directiveStart) {
        ngDevMode && assertDirectiveDef(tData[index]);
        registerPreOrderHooks(index, tData[index] as DirectiveDef<any>, tView);
      }
    } finally {
      if (factory.injectImpl) setInjectImplementation(previousInjectImplementation);
      setIncludeViewProviders(previousIncludeViewProviders);
      factory.resolving = false;
      leaveDI();
    }
  }
  return value;
}

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:

  try {
      if (moduleInjector) {
        return moduleInjector.get(token, notFoundValue, flags & InjectFlags.Optional);
      } else {
        return injectRootLimpMode(token, notFoundValue, flags & InjectFlags.Optional);
      }
  }
Limp mode injection by Ivy

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

/**
 * Injects `root` tokens in limp mode.
 *
 * If no injector exists, we can still inject tree-shakable providers which have `providedIn` set to
 * `"root"`. This is known as the limp mode injection. In such case the value is stored in the
 * `InjectableDef`.
 */
export function injectRootLimpMode<T>(
    token: Type<T>| InjectionToken<T>, notFoundValue: T | undefined, flags: InjectFlags): T|null {
  const injectableDef: ɵɵInjectableDef<T>|null = getInjectableDef(token);
  if (injectableDef && injectableDef.providedIn == 'root') {
    return injectableDef.value === undefined ? injectableDef.value = injectableDef.factory() :
                                               injectableDef.value;
  }
  if (flags & InjectFlags.Optional) return null;
  if (notFoundValue !== undefined) return notFoundValue;
  throw new Error(`Injector: NOT_FOUND [${stringify(token)}]`);
}
injectRootLimpMode in action, instance of our injectable will be created here!

The token and it's instance is stored in the InjectableDef - ɵprov property.

If you don't set providedIn to root, you will get this error in limp mode:

Limp services should be provided in root!

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:

  import { Component,  ɵrenderComponent as renderComponent } from '@angular/core';
  
  loadLimp() {
    if (!this.limp) {
      this.limp = import(`./limp/limp.component`)
        .then(({ LimpComponent }) => {
          renderComponent(LimpComponent);
        });
    }
  }
  
Limp component creation

Resolving module-level injectables

Now take a look at a common way of injectables resolution, when we have module-level injector. First, 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 the R3Injector.hydrate method:

        let record: Record<T>|undefined|null = this.records.get(token);
        if (record === undefined) {
          // No record, but maybe the token is scoped to this injector. Look for an injectable
          // def with a scope matching this injector.
          const def = couldBeInjectableType(token) && getInjectableDef(token);
          if (def && this.injectableDefInScope(def)) {
            // Found an injectable def and it's scoped to this injector. Pretend as if it was here
            // all along.
            record = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
          } else {
            record = null;
          }
          this.records.set(token, record); <--- Here token registered
        }
        // If a record was found, get the instance for it and return it.
        if (record != null /* NOT null || undefined */) {
          return this.hydrate(token, record); <--- Here token instance created
        }
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 the R3injector.get method while traversing module-level injectors, so your injectable should be provided in at least one. Otherwise you will get an error.

  private injectableDefInScope(def: ɵɵInjectableDef<any>): boolean {
    if (!def.providedIn) {
      return false;
    } else if (typeof def.providedIn === 'string') {
      return def.providedIn === 'any' || (def.providedIn === this.scope);
    } else {
      return this.injectorDefTypes.has(def.providedIn);
    }
  }
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 these articles also:

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! 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: