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