Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

How Angular Dependency Injection works under the hood

Dependency Injection (DI) is among the most beloved and highly effective options of Angular, and it occurs to be my private favourite as effectively. Understanding and mastering it could actually elevate your Angular expertise and grant you superpowers.
On this article, I’ll clarify what Dependency Injection is and delve into the way it operates inside Angular to supply a profound understanding.



What’s a Dependancy Injection

Let’s begin by inspecting an instance that does not use Dependency Injection:

@Element({
   //...
})
export class AppComponent {
  service = new RootService();
}
Enter fullscreen mode

Exit fullscreen mode

On this instance, we immediately instantiate the RootService utilizing the new key phrase, leading to a hardcoded dependency and a decent coupling between AppComponent and RootService. Whereas this method does work, it lacks flexibility, testability, and scalability in the long term, making it much less maintainable.

Now, let’s contemplate the identical instance utilizing Dependency Injection, the place you will recognise a well known Angular code snippet:

@Element({
   //...
})
export class AppComponent {
  service = inject(RootService);
// constructor(non-public service: RootService) {}
}
Enter fullscreen mode

Exit fullscreen mode

Notes: you need to use both the constructor or the inject perform, as each strategies have the identical underlying implementation.

As we are able to see, AppComponent is not immediately chargeable for instantiating RootService. As an alternative, it delegates this process to an exterior supply, which is chargeable for both returning an present occasion or creating a brand new occasion of the requested service.

We will simplify the code for this exterior supply, which could appear to be this:

export const inject = (searchClass: Class) => {
  const dependance = discover(searchClass)
  if(dependance) {
    return dependance;
  } else {
    return new searchClass();
  }
}
Enter fullscreen mode

Exit fullscreen mode

On this instance, AppComponent would not must have data about RootService. This reduces the coupling between lessons and their dependencies, making the code extra maintainable, testable, and reusable.

In Angular, this exterior supply is known as an Injector. And its implementation will be in comparison with a dictionary of data. The construction of a report appears to be like like this:

report:{
 //...
 [index]:{
   key: class RootService,
   worth: {
    manufacturing facility: ƒ RootService_Factory(t),
    worth: {}
   }
 //...
}
Enter fullscreen mode

Exit fullscreen mode

The Injector shops details about all injectable lessons, which incorporates something with a decorator comparable to @Injectable, @Element, @Pipe, and @Directive.

Returning to the earlier instance, when AppComponent requests RootService, the Injector iterates over its data to find the requested token. As soon as discovered, the Injector returns the worth if it isn’t undefined, indicating that the service has already been instantiated. In any other case, the Injector creates a brand new occasion utilizing the manufacturing facility perform.

As you may observe, the report is just an object, and the worth will be simply overridden. For instance, if we write the next code:

@Element({
   //...
   suppliers: [{ provide: RootService, useClass: OtherService }]
})
export class AppComponent {
  service = inject(RootService);
}
Enter fullscreen mode

Exit fullscreen mode

The Injector will override the worth property inside the RootService report:

report:{
 //...
 [index]:{
   key: class RootService,
   worth: {
    manufacturing facility: ƒ OtherService_Factory(t),
    worth: {}
   }
 //...
}
Enter fullscreen mode

Exit fullscreen mode

Which means that when AppComponent requests RootService, the Injector will present a brand new occasion of OtherService.

Observe: This instance simplifies how Angular’s Dependency Injection works, nevertheless it illustrates the underlying DI precept.

The subsequent part delves into extra superior features, revealing the inside workings of Angular’s DI system.



Angular Dependancy Injection

Angular has two classes of Injectors:

  1. EnvironmentInjector: This class consists of all international injectable lessons offered by means of the router, modules, or utilizing the providedIn: ‘root’ key phrase.

  2. NodeInjector: This class comprises all native injectable lessons present in every part or template.

It is essential to notice that every small piece of a view containing injectable lessons (known as LView) has its personal NodeInjector, and inside this NodeInjector, we are able to find all providers offered inside the part supplier array or any directives used inside that LView.

LView !== Element



Creation of EnvironmentInjector Tree

Once we bootstrap the applying, the bootstrapApplication perform known as in our major.ts file. This perform takes two parameters:

  • The basis Element
  • A listing of suppliers
bootstrapApplication(AppComponent, {
  suppliers: [GlobalService],
})
Enter fullscreen mode

Exit fullscreen mode

Below the hood, this perform will create three EnvironmentInjectors chained collectively:

  • NullInjector: That is the tip of the street. Its sole objective is to throw an error: “NullInjectorError: No supplier for …!!!”
  • PlatformInjector: It comprises an inventory of tokens that inform Angular in regards to the platform the applying is operating on, comparable to browser, server, net employee, and so forth. 

Instance: that is the place the InjectionToken DOCUMENT is created. As an illustration, if you’re on a browser, this token will return window.doc, whereas on a server, Angular will construct and supply a DOM utilizing Domino. It is essential to all the time work with the DOCUMENT token by injecting it as an alternative of utilizing window.doc. This ensures compatibility for those who ever must render your software from a server.

import { DOCUMENT } from '@angular/widespread';

@Element()
export class FooComponent {
  doc = inject(DOCUMENT) // ✅
  doc = window.doc // ❌
}
Enter fullscreen mode

Exit fullscreen mode

  • RootInjector: That is probably the most well-known of the three. It is the place all our international providers (injectables set as root) are saved.

Notes: If we refer again to the sooner instance, the GlobalService occasion might be positioned inside this injector.

All three of those injectors are chained collectively.



Creation of NodeInjector Tree

On this part, we are going to discover examples that you just seemingly encounter in your every day tasks. The primary half goals to supply a greater understanding of how the NodeInjector tree is created. (The NodeInjectorTree is sort of much like the ComponentTree however not strictly an identical.)

We are going to then see how Angular determines which dependencies to retrieve or create.

Observe: On this article, we is not going to talk about modules since most functions are anticipated to transition to standalone. Moreover, all new Angular functions might be set to standalone by default ranging from v17.



Tree Creation

Let’s look at how a NodeInjectorTree appears to be like like. We’ll start with a quite simple instance: a Mum or dad with one Baby.

@Element({
  template: `<little one />`,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Element({})
export class ChildComponent {}
Enter fullscreen mode

Exit fullscreen mode

This leads to the next tree:

Since ParentComponent and ChildComponent are annotated with @Element, it means they’re injectable. Thus, every part is saved inside its personal NodeInjector as follows. It is essential to notice that ChildComponent can inject ParentComponent, nevertheless it can’t inject itself, as this could create a round dependency.

Injector Tree Parent/Child with dependancies


Now, let’s add one other little one to the guardian:

@Element({
  template: `
    <little one />
    <little one />
   `,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Element({})
export class ChildComponent {}
Enter fullscreen mode

Exit fullscreen mode

The construction of each timber stays related.

Injector Tree with 2 childs


Nonetheless, let’s encapsulate one little one right into a div with a directive on it.

@Directive({
  selector: '[foo]',
  standalone: true,
})
export class FooDirective {}

@Element({
  selector: 'app-root',
  standalone: true,
  imports: [ChildComponent, FooDirective],
  template: `
    <div foo>
      <little one />
    </div>
    <little one />
  `,
})
export class ParentComponent {}
Enter fullscreen mode

Exit fullscreen mode

Injector Tree with child inside a div

Now, the InjectorTree begins to diverge from the ComponentTree. A brand new Injector has appeared. Since FooDirective is a kind of @Directive, it means it is injectable, and the primary ChildComponent can inject it.

From this instance, we are able to see {that a} NodeInjector isn’t related to a Element however with an LView (Logical View).

With these three examples, you’ve all it is advisable to perceive how the InjectorTree is constructed.

(Observe: Routing and ActivatedRoute might be defined in a follow-up article.)


Now, let’s discover other ways of offering an injectable service and the way Angular locates the occasion you might be injecting.



Element supplier

Throughout the part decorator, you’ve a property known as suppliers that lets you present an Injectable class, as illustrated beneath:

@Element({
  template: `...`,
  suppliers: [MyComponentService],
})
export class MyComponent {}
Enter fullscreen mode

Exit fullscreen mode

The service offered contained in the decorator might be saved inside the data of the NodeInjector of MyComponent. Please be aware that offering your service doesn’t instantiate it. A service is instantiated solely when it’s injected.

Let’s now look at which occasion is returned with two concrete examples:



Instance 1:

@Element({
  template: `
    <little one />
    <little one />
   `,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Element({
  suppliers: [MyService]
})
export class ChildComponent {
  myService = inject(MyService);
}
Enter fullscreen mode

Exit fullscreen mode

This leads to the next NodeInjectorTree:

Injector Tree with injectable service in Children

As we are able to see, MyService is current inside each ChildInjectors. When Angular creates the primary ChildComponent class, it’s going to request MyService from the DI system. The DI system will begin by looking contained in the report of ChildInjector, which appears to be like like this:

report:{
 //...
 [index]:{
   key: class MyService,
   worth: {
    manufacturing facility: ƒ MyService_Factory(t),
    worth: undefined
   }
 //...
}
Enter fullscreen mode

Exit fullscreen mode

Angular will iterate over all dictionary entries of the Injector to test if the important thing MyService is current. Since MyService is current inside this NodeInjector, it’s going to then test if it has already been instantiated, which isn’t the case for the reason that worth is undefined. On this case, a brand new occasion of MyService might be created and returned.

If the important thing wasn’t current contained in the report, the DI system will transfer to the subsequent Injector till discovering it or reaching the NullInjector, which can throw an error and terminate the applying.

The identical course of will repeat for the second occasion of ChildComponent. Angular will begin looking inside its personal NodeInjector, discover the important thing contained in the report, and since MyService has not been instantiated, a brand new occasion might be created.



Instance 2:

Now, let’s present MyService inside ParentComponent as an alternative of inside ChildComponent.

@Element({
  suppliers: [MyService]
  template: `
    <little one />
    <little one />
   `,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Element({})
export class ChildComponent {
  myService = inject(MyService);
}
Enter fullscreen mode

Exit fullscreen mode

Now, MyService is positioned contained in the report of ParentInjector.

Injector Tree with injectable service provided inside Parent COmponent

This time, when Angular creates the primary ChildComponent, it will not discover the important thing of MyService contained in the report of ChildInjector. Angular will then transfer as much as the subsequent Injector, which is ParentInjector. The report of ParentInjector appears to be like like this:

report:{
 //...
 [index]:{
   key: class MyService,
   worth: {
    manufacturing facility: ƒ MyService_Factory(t),
    worth: undefined
   }
 //...
}
Enter fullscreen mode

Exit fullscreen mode

Since MyService has not been instantiated but, a brand new occasion might be created and returned.

Nonetheless, issues are totally different when the second ChildComponent is created. Angular will traverse the NodeInjectorTree till reaching ParentInjector. However this time, the ParentInjector appears to be like like this:

report:{
 //...
 [index]:{
   key: class MyService,
   worth: {
    manufacturing facility: ƒ MyService_Factory(t),
    worth: MyService {
      prop1: 'xxx'
      // ...
    }
   }
 //...
}
Enter fullscreen mode

Exit fullscreen mode

The worth of MyService is not undefined. The DI System will return this occasion to the second ChildComponent. Which means that each ChildComponents are sharing the identical occasion of MyService, in contrast to within the earlier instance.

Observe: If ParentComponent was injecting MyService, the identical occasion can be shared amongst all three elements.



ProvidedIn: ‘root’

The providedIn: 'root' is among the mostly used injectable designs inside Angular functions, however not everybody absolutely understands the implications of those two phrases. This chapter goals to supply a transparent clarification.

Let’s create a really primary software with a guardian and a toddler:

@Element({
  template: `<little one />`,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Element({})
export class ChildComponent {
  service = inject(RootService);
}

@Injectable({ providedIn: 'root' })
export class RootService {}
Enter fullscreen mode

Exit fullscreen mode

Once we look at the NodeInjectorTree, we discover that RootService isn’t current in any of the data. It’s because Angular doesn’t embrace it in any Injector till a part really injects it.

Injector Tree with providedIn root

Observe: Within the context of lazy-loaded routes, RootService might get tree-shaken and bundled outdoors the principle bundle. This matter is past the scope of this text, however you may learn extra about it beneath.

Now, when Angular creates ChildComponent, it searches for RootService ranging from the ChildInjector and shifting up the tree, finally reaching the EnvironmentInjectorTree and extra exactly, the RootInjector.

Observe: The precise implementation is extra advanced, however for the sake of simplicity, we’ll present a high-level clarification right here.

When the DI system reaches the RootInjector, it searches for the RootService key, much like another NodeInjector. Nonetheless, it would not discover it there both. Not like NodeInjectors, earlier than shifting to the subsequent EnvironmentInjector, it compares the scope of the Injector with the scope of the service being injected.

The code beneath is a portion of the get perform of the RootInjector: (If you wish to see the complete perform, you may go right here)

let report: File<T>|undefined|null = this.data.get(token);
if (report === undefined) {
  // No report, however possibly the token is scoped to this injector. Search for an injectable
  // def with a scope matching this injector.
  const def = couldBeInjectableType(token) && getInjectableDef(token);
  if (def && this.injectableDefInScope(def)) {
    // Discovered an injectable def and it is scoped to this injector. Fake as if it was right here
    // all alongside.
    report = makeRecord(injectableDefOrInjectorDefFactory(token), NOT_YET);
  } else {
    report = null;
  }
  this.data.set(token, report);
}
Enter fullscreen mode

Exit fullscreen mode

First, it makes an attempt to retrieve the report of the searched token. If there isn’t a report, it checks if the service has an InjectableDef (the providedIn property). If the service has one and if the scope matches the scope of the present EnvironmentInjector (root in our case), a brand new report is created and added to the Injector, then a brand new occasion is returned.

The subsequent time a part requests RootService, the report might be current, and the identical occasion might be returned.

Observe: Whereas much less widespread, if you wish to present your service contained in the PlatformInjector, you may set your Injectable to providedIn: 'platform'.

Warning: In apply, setting the providedIn: 'root' property in your Injectable service signifies that your service might be a singleton. Nonetheless, for those who present your service inside the suppliers property of one in all your elements, this service might be added to the report of the NodeInjector of that part. Let’s examine an instance to raised perceive this:

@Element({})
export class ChildComponent {
  service = inject(RootService);
}

@Element({
  suppliers: [RootService]
})
export class FooComponent {
  service = inject(RootService);
}

@Element({
  template: `
    <little one />
    <foo />
  `,
  imports: [ChildComponent, FooComponent],
})
export class ParentComponent {}

// injectable service
@Injectable({ providedIn: 'root' })
export class RootService {}
Enter fullscreen mode

Exit fullscreen mode

Right here, we’ve a providedIn: 'root' RootService, which is injected inside each FooComponent and ChildComponent. Nonetheless, we offer RootService contained in the NodeInjector of ChildComponent. This offers us the next graph:

Injector Tree root service provided at component level

FooComponent may have an occasion of the service positioned contained in the RootInjector, whereas ChildComponent may have the one from its personal Injector. This may be deceptive as a result of by observing the service, one would possibly assume that each elements share the identical international occasion, which isn’t the case on this instance.

In abstract, providedIn: 'root' is just an data for Angular to create a report inside RootInjector solely and provided that the service attain that time whereas looking for it contained in the InjectorTree.


I actually hope that the Dependency Injection System of Angular will not maintain any secrets and techniques for you. You need to now be capable to harness its energy to create distinctive functions and perceive whether or not an occasion of a service might be shared or distinctive.

You’ll be able to anticipate me to write down follow-up articles on the next topics:

  • Dependency Injection inside Routed Parts
  • Injection Flags: Host, Self, SkipSelf, and Non-obligatory
  • All of the choices for overriding inside the DI: useClass, useValue, useFactory, useExisting

If you want to find out about the rest, please do not hesitate to depart a remark.

If you wish to enhance your Angular talent, go try Angular Challenges. It teams a set of challenges about Angular and its ecosystem. 


You’ll find me on Twitter or Github. Do not hesitate to succeed in out to me if in case you have any questions.



Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?