Dependency Injection

dependency-injection

About

The two main roles of DI in Angular are:

  • Dependency consumer;

  • Dependency provider.

Angular facilitates the interaction between these two roles using an abstraction called Injector.

When a dependency is requested, the Injector checks its registry to see if there is an instance already available there.

If not, a new instance is created and stored in the registry.

Angular can create an application-wide injector known as root injector, during the bootstrap process.

Providing a Dependency

The DI system relies internally on a runtime context where the current injector is available. This means that injectors can only work when code is executed in such context.

Although there are specific places that have injection context, you can also create an injection context with runInInjectionContext.

At application root level using providedIn

@Injectable({
    providedIn: 'root'
})

Angular creates a single, shared instance of the service, and injects it into any class you ask for it.

Using providedIn: 'root' when creating a service, allows injecting the service into all other classes.

If you want to call a Service A inside another Service B, then Service A will have to be providedIn: 'root'.

Will use the root injector.

At component level

You can provide services at @Component level by using the providers: [] property in the component decorator.

@Component({
    providers: [SomeService]
})

The default behavior is for the injector to instantiate that class using new operator.

In this case, the service becomes available to all instances of this component and other components and directives used in the Template.

Will use component-specific injector.

You can configure DI to associate a Service provider token with a different class or other values like.

The expanded provider configuration is an object literal with two properties:

  • The provide property holds the token that serves as the key for consuming the dependency value.

  • The second property is a provider definition object, which tells the injector how to create the dependency value. This can be one of the following:

    • useClass

    • useExisting

    • useFactory

    • useValue

useClass

[{ provide: Logger, useClass: TestLogger }]

Tells Angular DI to instanciate a provided class when a dependency is injected.

[
    UserService, // dependency needed in `EvenBetterLogger`.
    { provide: Logger, useClass: EvenBetterLogger }
]

useExisting

Allows you to mapo one token to another. (Alias a token and reference any existing one)

In effect, the first token is an alias for the service associated with the second token, creating two ways to access the same service object.

[
    NewLogger,
    // Alias OldLogger with reference to NewLogger
    { provide: OldLogger, useExisting: NewLogger},
]

useFactory

Allows you to create a dependency object by calling a factory function. With this approach, you can create a dynamic value based on information available in the DI and elsewhere in the app.

Check the example in the docs.

useValue

Lets you associate a static value with a DI token.

The following example show how to inject global data variables. (APP_DATA_TOKEN and APP_DATA don't have to be created in the same file)

import { InjectionToken } from '@angular/core';

interface AppData {
    ip: string;
    env: string;
    ...
}

// `InjectionToken` requires a parameter which is just a description for the Token
const APP_DATA_TOKEN = new InjectionToken<AppData>('app.data description');
const APP_DATA: AppData = { ip: '127.0.0.1', env: 'development' };

@Component({
    providers: [{ provide: APP_DATA_TOKEN, useValue: APP_DATA }]
})
export class Component {
    constructor(@Inject(APP_DATA_TOKEN) appData: AppData) {}
}

At application root level using ApplicationConfig

export const appConfig: ApplicationConfig = {
    providers: [{ provide: HeroService }],
};

The service will be available to all components, directives and pipes.

Injectors have rules that you can leverage to achieve the desired visibility of injectables in your application.

There may be sections in your application that work completely independent, with its own local copies of the services and other dependencies that it needs.

With hierarchical DI, you can:

  • Isolate sections of the application and give them their own private dependencies.

  • Have parent components share certain dependencies with its child components only.

Resolution Rules (Sequence)

  1. When a Component declares a dependency, Angular tries to satisfy the dependency with its own ElementInjector.

  2. If the components lacks the provider, it passes the request up to its parent ElementInjector.

  3. This request keeps forwarding up until Angular finds an injector that can handle the request or runs out of ancestors ElementInjector.

  4. If it still could not find it, it goes back to the element where the request originated and looks in the EnvironmentInjector hierarchy.

  5. If it still does not find it, it throws an error.

If you have registered a provider for the same DI token at different levels, the first one Angular encounters is the one it uses to resolve the dependency.

Injector Hierarchies

Angular has two injector hierarchies:

EnvironmentInjector

Can be configured in one of two ways:

  • The @Injectable({ providedIn: root | plataform }) property to refer to root or plataform.

  • The ApplicationConfig providers array.

Tree-Shaking

Only available with @Injectable({ providedIn }) method.

ElementInjector

Angular creates ElementInjector hierarchies implicitly for each DOM element.

Providing a service in the @Component using providers: [] or viewProviders property configures an ElementInjector.

@Component({
    providers: []
    // Or
    viewProviders: []
})

Services provided like this are available at that component instance, and may be visible at child component/directives based on visibility rules. (Components and directive on the same element share an injector)

When the component instance is destroy, so is that service instance.

Angular's resolution behavior can be modified.

Use them when injecting services, in:

  • Component class constructor.

  • Or the inject() configuration.

@Optional()

Allows Angular to consider a service you inject to be optional.

So if it cannot be resolved at runtime, Angular resolves the service as null, rather than throwing an error.

export class Component {
    constructor(@Optional() private logger?: LoggerService) {}
}

@Self()

Use it so that Angular will only look at the ElementInjector for the current component or directive. (It will not go up to it's ancestors)

A good case for @Self() is to inject a service but only if it is available on the current host element.

It's good practice to use @Self() with @Optional().

In the following example, if LoggerService is provided inside Component class it will be availble, otherwise it will be null, because it won't check if the parent component of Component has it.

export class Component {
    constructor(@Self() @Optional() private logger?: LoggerService) {}
}

@SkipSelf()

It is the opposite of @Self(), so Angular starts looking in the parent ElementInjector, rather than in the current one.

Also use @SkipSelf() with @Optional() to prevent an error if the value is null.

In the following example, any instance of LoggerService provided inside Component class will be ignored. Angular will try to get it from Component parent, and if the parent don't provide it then it will be null.

export class Component {
    constructor(@SkipSelf() @Optional() private logger?: LoggerService) {}
}

@Host()

Lets you designate a component as the last stop in the injector tree when searching for providers.

Even if there is a service instance further up the tree, Angular won't continue looking.

Last updated