How Angular Dependency Injection works under the hood
Tags: angular, dependancyinjection
Published: 2023-11-16 13:23:11

Dependency Injection (DI) is one of the most beloved and powerful features of Angular, and it happens to be my personal favorite as well. Understanding and mastering it can elevate your Angular skills and grant you superpowers.
In this article, I will explain what Dependency Injection is and delve into how it operates within Angular to provide a profound understanding.

## What is a Dependancy Injection
Let's start by examining an example that doesn't use Dependency Injection:

```ts
@Component({
   //...
})
export class AppComponent {
  service = new RootService();
}
```

In this example, we directly instantiate the `RootService` using the `new` keyword, resulting in a hardcoded dependency and a tight coupling between `AppComponent` and `RootService`. While this approach does work, it lacks flexibility, testability, and scalability in the long run, making it less maintainable.

Now, let's consider the same example using Dependency Injection, where you'll recognise a well-known Angular code snippet:

```ts
@Component({
   //...
})
export class AppComponent {
  service = inject(RootService);
// constructor(private service: RootService) {}
}
```

> _**Notes:** you can use either the constructor or the `inject` function, as both methods have the same underlying implementation._

As we can see, `AppComponent` is no longer directly responsible for instantiating `RootService`. Instead, it delegates this task to an external source, which is responsible for either returning an existing instance or creating a new instance of the requested service.

We can simplify the code for this external source, which might look like this:

```ts
export const inject = (searchClass: Class) => {
  const dependance = find(searchClass)
  if(dependance) {
    return dependance;
  } else {
    return new searchClass();
  }
}
```

In this example, `AppComponent` doesn't need to have knowledge about `RootService`. This reduces the coupling between classes and their dependencies, making the code more maintainable, testable, and reusable.

In Angular, this external source is referred to as an **Injector**. And its implementation can be compared to a dictionary of records. The structure of a record looks like this:

```ts
record:{
 //...
 [index]:{
   key: class RootService,
   value: {
    factory: ƒ RootService_Factory(t),
    value: {}
   }
 //...
}
```

The Injector stores information about all injectable classes, which includes anything with a decorator such as `@Injectable`, `@Component`, `@Pipe`, and `@Directive`.

Returning to the previous example, when `AppComponent` requests `RootService`, the Injector iterates over its records to locate the requested token. Once found, the Injector returns the value if it's not undefined, indicating that the service has already been instantiated. Otherwise, the Injector creates a new instance using the factory function.

As you can observe, the record is simply an object, and the value can be easily overridden. For example, if we write the following code:

```ts
@Component({
   //...
   providers: [{ provide: RootService, useClass: OtherService }]
})
export class AppComponent {
  service = inject(RootService);
}
```

The Injector will override the `value` property within the `RootService` record:

```ts
record:{
 //...
 [index]:{
   key: class RootService,
   value: {
    factory: ƒ OtherService_Factory(t),
    value: {}
   }
 //...
}
```

This means that when `AppComponent` requests `RootService`, the Injector will provide a new instance of `OtherService`.

> _**Note:** This example simplifies how Angular's Dependency Injection works, but it illustrates the underlying DI principle._

The next section delves into more advanced aspects, revealing the inner workings of Angular's DI system.

## Angular Dependancy Injection
Angular has two categories of Injectors:

1. **EnvironmentInjector:** This category includes all global injectable classes provided through the router, modules, or using the providedIn: 'root' keyword.
 
2. **NodeInjector:** This category contains all local injectable classes found in each component or template.

It's important to note that each small piece of a view containing injectable classes (referred to as **LView**) has its own NodeInjector, and within this NodeInjector, we can locate all services provided within the component provider array or any directives used within that LView.

> **LView !== Component**

### Creation of EnvironmentInjector Tree
When we bootstrap the application, the `bootstrapApplication` function is called in our `main.ts` file. This function takes two parameters:

- The root Component
- A list of providers

```ts
bootstrapApplication(AppComponent, {
  providers: [GlobalService],
})
```

Under the hood, this function will create three EnvironmentInjectors chained together:

- **NullInjector**: This is the end of the road. Its sole purpose is to throw an error: _"NullInjectorError: No provider for …!!!"_
- **PlatformInjector**: It contains a list of tokens that inform Angular about the platform the application is running on, such as browser, server, web worker, etc. 

**Example:** this is where the `InjectionToken` `DOCUMENT` is created. For instance, if you are on a browser, this token will return `window.document`, whereas on a server, Angular will build and provide a DOM using Domino. It's crucial to always work with the `DOCUMENT` token by injecting it instead of using `window.document`. This ensures compatibility if you ever need to render your application from a server.

```ts
import { DOCUMENT } from '@angular/common';

@Component()
export class FooComponent {
  document = inject(DOCUMENT) // ✅
  document = window.document // ❌
}
```

- **RootInjector**: This is the most well-known of the three. It's where all our global services _(injectables set as root)_ are stored.

**Notes:** If we refer back to the earlier example, the `GlobalService` instance will be located within this injector.

All three of these injectors are chained together.

### Creation of NodeInjector Tree
In this section, we will explore examples that you likely encounter in your daily projects. The first part aims to provide a better understanding of how the NodeInjector tree is created. _(The NodeInjectorTree is almost similar to the ComponentTree but not strictly identical.)_

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

> _**Note:** In this article, we will not discuss modules since most applications are expected to transition to standalone. Furthermore, all new Angular applications will be set to standalone by default starting from v17._

#### Tree Creation
Let's examine how a NodeInjectorTree looks like. We'll begin with a very simple example: a Parent with one Child.

```ts
@Component({
  template: `<child />`,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Component({})
export class ChildComponent {}
```

This results in the following tree:


![Injector tree parent/child](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0rjiozbqrw3zlhj24alr.png)

Since `ParentComponent` and `ChildComponent` are annotated with `@Component`, it means they are injectable. Thus, each component is stored within its own NodeInjector as follows. It's important to note that `ChildComponent` can inject `ParentComponent`, but it cannot inject itself, as this would create a circular dependency.


![Injector Tree Parent/Child with dependancies](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bgu3jzfupn20r9q1ua4r.png)

---

Now, let's add another child to the parent:

```ts
@Component({
  template: `
    <child />
    <child />
   `,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Component({})
export class ChildComponent {}
```
The structure of both trees remains similar.


![Injector Tree with 2 childs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u45gwtj0gwu8zr3o18wn.png)

---

However, let's encapsulate one child into a div with a directive on it.

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

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ChildComponent, FooDirective],
  template: `
    <div foo>
      <child />
    </div>
    <child />
  `,
})
export class ParentComponent {}
```

![Injector Tree with child inside a div](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ouyppv1gag9btl9rp7es.png)

Now, the InjectorTree begins to diverge from the ComponentTree. A new Injector has appeared. Since `FooDirective` is a type of `@Directive`, it means it's injectable, and the first `ChildComponent` can inject it.

From this example, we can see that a NodeInjector is not associated with a Component but with an LView (Logical View).

With these three examples, you have all you need to understand how the InjectorTree is built.

_(**Note:** Routing and ActivatedRoute will be explained in a follow-up article.)_

---

Now, let's explore different ways of providing an injectable service and how Angular locates the instance you are injecting.

### Component provider
Within the component decorator, you have a property called `providers` that allows you to provide an Injectable class, as illustrated below:

```ts
@Component({
  template: `...`,
  providers: [MyComponentService],
})
export class MyComponent {}
```

The service provided inside the decorator will be stored within the records of the NodeInjector of `MyComponent`. **Please note that providing your service does not instantiate it. A service is instantiated only when it is injected.**

Let's now examine which instance is returned with two concrete examples:

#### Example 1:

```ts
@Component({
  template: `
    <child />
    <child />
   `,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Component({
  providers: [MyService]
})
export class ChildComponent {
  myService = inject(MyService);
}
```
This results in the following NodeInjectorTree:

![Injector Tree with injectable service in Children](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bowee2g7oqsiuu2za2zv.png)


As we can see, `MyService` is present inside both `ChildInjectors`. When Angular creates the first `ChildComponent` class, it will request `MyService` from the DI system. The DI system will start by searching inside the record of `ChildInjector`, which looks like this:

```ts
record:{
 //...
 [index]:{
   key: class MyService,
   value: {
    factory: ƒ MyService_Factory(t),
    value: undefined
   }
 //...
}
```

Angular will iterate over all dictionary entries of the Injector to check if the key `MyService` is present. Since `MyService` is present inside this `NodeInjector`, it will then check if it has already been instantiated, which is not the case since the value is undefined. In this case, a new instance of `MyService` will be created and returned.

If the key wasn't present inside the record, the DI system will move to the next Injector until finding it or reaching the `NullInjector`, which will throw an error and terminate the application.

The same process will repeat for the second instance of `ChildComponent`. Angular will start searching inside its own `NodeInjector`, find the key inside the record, and since `MyService` has not been instantiated, a new instance will be created.

#### Example 2:
Now, let's provide `MyService` inside `ParentComponent` instead of inside `ChildComponent`.

```ts
@Component({
  providers: [MyService]
  template: `
    <child />
    <child />
   `,
  imports: [ChildComponent],
})
export class ParentComponent {}

@Component({})
export class ChildComponent {
  myService = inject(MyService);
}
```
Now, `MyService` is located inside the record of `ParentInjector`.

![Injector Tree with injectable service provided inside Parent COmponent](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a06sk5elssbcr8nmv4q.png)

This time, when Angular creates the first `ChildComponent`, it won't find the key of `MyService` inside the record of `ChildInjector`. Angular will then move up to the next Injector, which is `ParentInjector`. The record of `ParentInjector` looks like this:

```ts
record:{
 //...
 [index]:{
   key: class MyService,
   value: {
    factory: ƒ MyService_Factory(t),
    value: undefined
   }
 //...
}
```

Since `MyService` has not been instantiated yet, a new instance will be created and returned.

However, things are different when the second `ChildComponent` is created. Angular will traverse the `NodeInjectorTree` until reaching `ParentInjector`. But this time, the ParentInjector looks like this:

```ts
record:{
 //...
 [index]:{
   key: class MyService,
   value: {
    factory: ƒ MyService_Factory(t),
    value: MyService {
      prop1: 'xxx'
      // ...
    }
   }
 //...
}
```

The value of `MyService` is no longer undefined. The DI System will return this instance to the second `ChildComponent`. This means that both `ChildComponents` are sharing the same instance of `MyService`, unlike in the previous example.

**Note:** If `ParentComponent` was injecting `MyService`, the same instance would be shared among all three components.

### ProvidedIn: 'root'
The `providedIn: 'root'` is one of the most commonly used injectable designs within Angular applications, but not everyone fully understands the implications of these two words. This chapter aims to provide a clear explanation.

Let's create a very basic application with a parent and a child:

```ts
@Component({
  template: `<child />`,
  imports: [ChildComponent],
})
export class ParentComponent {}

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

@Injectable({ providedIn: 'root' })
export class RootService {}
```

When we examine the NodeInjectorTree, we find that `RootService` is not present in any of the records. This is because **Angular does not include it in any Injector until a component actually injects it.**

![Injector Tree with providedIn root](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9lo2v3ay5bpzv9y2tgw.png)


**Note:** In the context of lazy-loaded routes, `RootService` may get tree-shaken and bundled outside the main bundle. This topic is beyond the scope of this article, but you can read more about it below.

{% embed https://dev.to/this-is-angular/mastering-injectable-services-a-comprehensive-guide-3kjg %}

Now, when Angular creates `ChildComponent`, it searches for `RootService` starting from the `ChildInjector` and moving up the tree, eventually reaching the `EnvironmentInjectorTree` and more precisely, the `RootInjector`.

> _**Note:** The exact implementation is more complex, but for the sake of simplicity, we'll provide a high-level explanation here._

When the DI system reaches the `RootInjector`, it searches for the `RootService` key, similar to any other `NodeInjector`. However, it doesn't find it there either. Unlike `NodeInjectors`, before moving to the next `EnvironmentInjector`, it compares the scope of the Injector with the scope of the service being injected.

The code below is a portion of the `get` function of the `RootInjector`: _(If you want to see the full function, you can go [here](https://dev.to/this-is-angular/mastering-injectable-services-a-comprehensive-guide-3kjg))_

```ts
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);
}
```

First, it attempts to retrieve the record of the searched token. If there is no record, 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 current `EnvironmentInjector` _(root in our case)_, a new record is created and added to the Injector, then a new instance is returned.

The next time a component requests `RootService`, the record will be present, and the same instance will be returned.

> _**Note:** While less common, if you want to provide your service inside the `PlatformInjector`, you can set your Injectable to `providedIn: 'platform'`._

**Warning:** In practice, setting the `providedIn: 'root'` property for your Injectable service signifies that your service will be a singleton. However, if you provide your service within the `providers` property of one of your components, this service will be added to the record of the `NodeInjector` of that component. Let's see an example to better understand this:

```ts
@Component({})
export class ChildComponent {
  service = inject(RootService);
}

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

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

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

Here, we have a `providedIn: 'root'` `RootService`, which is injected inside both `FooComponent` and `ChildComponent`. However, we provide `RootService` inside the `NodeInjector` of `FooComponent`. This gives us the following graph:

![Injector Tree root service provided at component level](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kpjgbqzd5asd5g27no21.png)

`ChildComponent` will have an instance of the service located inside the `RootInjector`, whereas `FooComponent` will have the one from its own Injector. This can be misleading because by observing the service, one might assume that both components share the same global instance, which is not the case in this example.

In summary, `providedIn: 'root'` is only an information for Angular to create a record inside RootInjector only and only if the service reach that point while searching for it inside the InjectorTree.

---

I really hope that the Dependency Injection System of Angular will no longer hold any secrets for you. You should now be able to harness its power to create exceptional applications and understand whether an instance of a service will be shared or unique.

You can expect me to write follow-up articles on the following subjects:

- Dependency Injection inside Routed Components
- Injection Flags: Host, Self, SkipSelf, and Optional
- All the options for overriding within the DI: useClass, useValue, useFactory, useExisting

If you would like to learn about anything else, please don't hesitate to leave a comment.

> If you want to improve your Angular skill, go check out [Angular Challenges](https://angular-challenges.vercel.app/). It groups a set of challenges about Angular and its ecosystem. 

---

You can find me on [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge). Don't hesitate to reach out to me if you have any questions.