Mastering Angular’s Hierarchical Dependency Injection with inject() Function
Tags: angular, webdev
Published: 2023-08-26 17:47:20

##Introduction
Before Angular 14, Angular achieves hierarchical dependency injection by injecting services in constructor and applying combination of @Host, @Self, @SkipSelf() and @Optional() decorators. In Angular 14, Angular team introduces `inject()` and it accepts inject options that can achieve the results. In this blog post, I am going to illustrate how to pass different inject options to `inject()` in order to exercise fine-grained dependency injection. Mastering hierarchical dependency injection is not difficult after we understand host, self, skipSelf and optional values.

###Understand inject option and dependency injection decorators
Before mastering hierarchical dependency injection, we should know the available decorators and their counterparts.

```typescript
// inject option

{
   optional?: boolean,
   host?: boolean,
   self?: boolean,
   skipSelf?: boolean
}
```

- @Host() – the property name is host and it is a boolean value. When used, Angular finds the service in the providers array of this component and stops. When service does not exist, error is thrown unless optional is used
- @Self() – the property name is self and it is a boolean value. When used, Angular finds the service in the providers array of the component. When service does not exist, error is thrown unless optional is used
- @SkipSelf() – the property name is skipSelf and it is a boolean value. When used, Angular finds the service in the providers array of parent component. If parent cannot provide the service, then Angular will find it in the providers array of its ancestors or root injector. When service does not exist, error is thrown unless optional is used
- @Optional() – the property name is optional and it is a boolean value. When used, the service may or may not provide to the component. If the component belongs to a component tree, Angular will find the service in the providers array of its ancestors or root injector. When service does not exist, no error is thrown and the service is undefined

###Define a service for hierarchical dependency injection
```typescript
import { Injectable } from "@angular/core";

@Injectable()
export class MessageService {

  message() {
    return 'This is MessageService';
  }
}
```

`MessageService` is a service but `@Injectable` does not have `{ providedIn: root }` option. Therefore, it does not exist in root injector. If component wants to inject `MessageService`, it will require to provide inject option in `inject()` function.

Next, I am going to explore different inject optional flags in the situation of single component.

###Provide service with optional flag
```typescript
// optional.component.ts

@Component({
  selector: 'app-optional',
  standalone: true,
  template: `
    <div>
      <p>Optional Component</p>
      <p>Msg: {{ msg }}</p>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OptionalComponent {
  service? = inject(MessageService, { optional:  true });

  msg = this.service ? this.service.message() : 'Cannot inject MessageService and optional flag enabled.'
}
```

In `OptionalComponent`, the second parameter of `inject()` is `{ optional: true }`. Therefore, MessageService can be optional. The component does not have providers array to provide MessageServie. Therefore, this.service is null and the value of msg is ‘Cannot inject MessageService and optional flag enabled.’

###Provide service with self flag
```typescript
// self.component.ts

@Component({
  selector: 'app-self',
  standalone: true,
  template: `
    <div>
      <p>Self Component</p>
      <p>Msg: {{ msg }}</p>
    </div>
  `,
  providers: [
    {
      provide: MessageService,
      useFactory: () => ({
        message() {
          return 'Provide MessageService in SelfComponent';
        }
      }),
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelfComponent {
  service = inject(MessageService, { self: true });
  msg = this.service.message();
}
```

In `SelfComponent`, the second parameter of `inject()` is `{ self: true }`. The component is the only candidate to provide MessageService. Fortunately, the providers array provides MessageServie; therefore, this.service is defined and the value of msg is ‘Provide MessageService in SelfComponent.’

To avoid error, I can provide optional property together with self.

```typescript
// self-optional.component.ts

@Component({
  selector: 'app-self-optional',
  standalone: true,
  template: `
    <div>
      <p>Self Optional Component</p>
      <p>Msg: {{ msg }}</p>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SelfOptionalComponent {
  service? = inject(MessageService, { self: true, optional: true });
  msg = this.service?.message() ?? 'Component does not inject MessageService itself and optional message is shown';
}
```

`this.service` is null and the value of msg is ‘Component does not inject MessageService itself and optional message is shown’

###Provide service with skipSelf flag
```typescript
// skip-self-option.component.ts

@Component({
  selector: 'app-skip-self-optional',
  standalone: true,
  template: `
    <div>
      <p>SkipSelf Optional Component</p>
      <p>Msg: {{ msg }}</p>
    </div>
  `,
  providers: [
    {
      provide: MessageService,
      useFactory: () => ({
        message() {
          return 'SkipSelf flag is enabled, you should not see this message';
        }
      }),
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SkipSelfOptionalComponent {
  service? = inject(MessageService, { skipSelf: true, optional: true });

  msg = this.service?.message() ?? 'skipSelf enabled and cannot inject MessageService, default message shown';
}
```

When using `inject(MessageService, { skipSelf: true })`, I expect the component is a child of a parent component. Otherwise, error message is thrown.

In `SkipSelfOptionalComponent`, the second parameter of `inject()` is `{ skipSelf: true, option: true }`. Therefore, the component does not throw error and this.msg is 'skipSelf enabled and cannot inject MessageService, default message shown'

###Provide service with host flag
In single component case, host property has the same results as self property.

```typescript
// host.component.ts

@Component({
  selector: 'app-host',
  standalone: true,
  template: `
    <div>
      <p>Host Component</p>
      <p>Msg: {{ msg }}</p>
      <app-skip-self></app-skip-self>
      <app-self-optional></app-self-optional>
      <app-optional></app-optional>
    </div>
  `,
  providers: [
    {
      provide: MessageService,
      useFactory: () => ({
        message() {
          return 'Host component of SkipSelfComponent.  Both components should see this message';
        }
      }),
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HostComponent {
  service = inject(MessageService, { host: true });
  msg = this.service.message();
}
```

The value of `this.msg` is 'Host component of SkipSelfComponent. Both components should see this message'

```typescript
// host-optional.componen.ts

@Component({
  selector: 'app-host-optional',
  standalone: true,
  template: `
    <div>
      <p>Host Optional Component</p>
      <p>Msg: {{ msg }}</p>
      <app-skip-self-optional></app-skip-self-optional>
      <app-self-optional></app-self-optional>
      <app-optional></app-optional>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HostOptionalComponent {
  service? = inject(MessageService, { host: true, optional: true });
  
  msg = this.service?.message() ?? 'Host Optional component returns default message';  
}
```

On the other hand, the value of `this.msg` is 'Host Optional component returns default message'

As a single component, dependency injection is straightforward because the component either provides the service or return null. In the next section, I am going to illustrate how hierarchy dependency injection affects the value of msg in complex component.


###Hierarchical dependency injection in complex components
```typescript
// parent.component.ts

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [HostComponent, HostOptionalComponent],
  template: `
    <div>
      <p>Parent Component</p>
      <p>Msg: {{ msg }}</p>
      <app-host></app-host>
      <app-host-optional></app-host-optional>
    </div>
  `,
  providers: [
    {
      provide: MessageService,
      useFactory: () => ({
        message() {
          return 'Message in Parent component';
        }
      }),
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ParentComponent {
  msg = inject(MessageService).message();
}
```

`ParentComponent` is the parent of `HostComponent` and `HostOptionalComponent`. `HostComponent` is the parent of `SkipSelfComponent`, `SelfOptionalComponent` and `OptionalComponent`. Similarly, `HostOptionalComponent` is the parent of `SkipSelfOptionalComponent`, `SelfOptionalComponent` and `OptionalComponent`. As children of complex components, `SkipSelfComponent`, `SkipSelfOptionalComponent`, and `OptionalComponent` render different values.

Let me explain further.

For the case of `SkipSelfComponent`, `HostComponent` is its parent and it provides `MessageService` in the providers array. Therefore, `SkipSelfComponent` injects the `MessageService` of `HostComponent` and displays “Host component of SkipSelfComponent. Both components should see this message”.

The same reasoning also applies to `OptionalComponent`. It does not provide `MessageService` and injects the service from `HostComponent`. Therefore, `OptionalComponent` displays the same text.

For the case of `SkipSelfOptionalComponent`, `HostOptionalComponent` is its parent and it does not provide `MessageService`. Angular goes one step further to find the service in its grandparent, `ParentComponent`. Fortunately, `ParentComponent` provides the service in the providers array. `SkipSelfOptionalComponent` injects `MessageService` from `ParentComponent` and displays “Message in Parent component”.

When `OptionalComponent` is the sibling of `SkipSelfOptionalComponent`, it also injects `MessageService` from `ParentComponent` and displays “Message in Parent component”. `OptionalComponent` renders different results when it is a child of `HostComponent` and `HostOptionalComponent` respectively

`SelfOptionalComponent` always look up `MessageService` in its providers array. Therefore, it renders “Component does not inject MessageService itself and optional message is shown” and ignores whose its parent is.

The following Stackblitz repo shows the final results:
{%embed https://stackblitz.com/edit/stackblitz-starters-3mwvqo?file=src%2Fmain.ts %}


This is the end of the blog post and I hope you like the content and continue to follow my learning experience in Angular and other technologies.


##Resources:

- Github Repo: https://github.com/railsstudent/ng-provider-demo
- Stackblitz: https://stackblitz.com/edit/stackblitz-starters-3mwvqo?file=src%2Fmain.ts
- Hierarchical dependency injection: https://angular.io/guide/hierarchical-dependency-injection