Mastering Angular’s Hierarchical Dependency Injection with inject() Function

Reading Time: 5 minutes

Loading

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.

let's go

Understand inject option and dependency injection decorators

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

// 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

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

// 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

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

// 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

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

// 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'

// 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

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

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:

  1. Github Repo: https://github.com/railsstudent/ng-provider-demo
  2. Stackblitz: https://stackblitz.com/edit/stackblitz-starters-3mwvqo?file=src%2Fmain.ts
  3. Hierarchical dependency injection: Angular official documentation