This article will take you through the application of dependency injection in Angular and some implementation principles, including

  • Scenarios for useFactory, useClass, useValue, and useExisting providers

  • ModuleInjector and ElementInjector have different levels of injector meaning

  • Defines the difference between providers in @Injectable() and @NgModule()

  • Use of @optional (), @self (), @skipself (), @host () decorators

  • Application scenarios for MUTI (multi-provider)

If you’re not sure what dependency injection is, read this article to get a better understanding of Angular dependency injection: What is dependency injection

UseFactory, useClass, useValue, useExistingproviderApplication scenarios of

Below, we illustrate the usage scenarios of several providers through practical examples.

1. UseFactory Specifies the factory provider

One day, we received a request to implement a local storage feature and inject it into an Angular application to make it globally available on the system

Firstly, write the service class storage.service.ts to realize its storage function

// storage.service.ts
export class StorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') | | {}; } set(key:string.value: ITokenModel | null) :boolean {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  }

  remove(key: string) {
    localStorage.removeItem(key); }}Copy the code

If you try it right away in user.component.ts

// user.component.ts
@Component({
  selector: 'app-user'.templateUrl: './user.component.html'.styleUrls: ['./user.component.css']})export class CourseCardComponent  {
  constructor(private storageService: StorageService){... }... }Copy the code

You should see an error like this:

NullInjectorError: No provider for StorageService!
Copy the code

Obviously, we didn’t add StorageService to Angular’s dependency injection system. Angular can’t get the Provider for the StorageService dependency, so it can’t instantiate the class, and it can’t call the methods in the class.

Next, we manually add a Provider in line with the idea of missing and supplementing. Modify the storage.service.ts file as follows

// storage.service.ts
export class StorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') | | {}; }set(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  remove(key: string) {
    localStorage.removeItem(key); }}// Add a factory function to instantiate StorageService
export storageServiceProviderFactory(): StorageService {
  return new StorageService();
}
Copy the code

With the code above, we already have a Provider. Then, the next question is if the presents each scan to StorageService this dependency, let its execution storageServiceProviderFactory method, to create an instance

This leads to the next point InjectionToken

In a service class, we often need to add multiple dependencies to keep the service available. The InjectionToken is a unique identifier for each dependency and allows Angular’s dependency injection system to find the Provider for each dependency.

Next, we manually add an InjectionToken

// storage.service.ts
import { InjectionToken } from '@angular/core';

export class StorageService {
  get(key: string) {
    return JSON.parse(localStorage.getItem(key) || '{}') | | {}; }set(key: string, value: any) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  remove(key: string) {
    localStorage.removeItem(key); }}export storageServiceProviderFactory(): StorageService {
  return new StorageService();
}

// Add the StorageServiced InjectionToken
export const STORAGE_SERVICE_TOKEN = new InjectionToken<StorageService>('AUTH_STORE_TOKEN');
Copy the code

Ok, we have Provider and InjectionToken for StorageService.

Next, we need a configuration that Angular’s Dependency injection system recognizes when it scans the StorageService(Dependency), According to STORAGE_SERVICE_TOKEN (InjectionToken) to find the corresponding storageServiceProviderFactory (Provider), and then create the instance of dependencies. Here’s what we do in the @NgModule() decorator in module:

// user.module.ts
@NgModule({
  imports: [...]. .declarations: [...]. .providers: [{provide: STORAGE_SERVICE_TOKEN, // The dependency associated with the InjectionToken, used to control the factory function call
      useFactory: storageServiceProviderFactory, // This factory function is called when a dependency needs to be created and injected
      deps: [] // If StorageService has other dependencies, add them here}]})export class UserModule {}Copy the code

At this point, we have completed the implementation of the dependency. Finally, Angular needs to know where to inject. Angular provides the @Inject decorator to identify it

// user.component.ts
@Component({
  selector: 'app-user'.templateUrl: './user.component.html'.styleUrls: ['./user.component.css']})export class CourseCardComponent  {
  constructor(@Inject(STORAGE_SERVICE_TOKEN) private storageService: StorageService){... }... }Copy the code

At this point, we can call the StorageService method in user.component.ts

2. UseClass class provider

emm… Do you think this is too complicated? In practice, most of our scenarios do not require manual creation of providers and InjectionTokens. As follows:

// user.component.ts
@Component({
  selector: 'app-user'.templateUrl: './user.component.html'.styleUrls: ['./user.component.css']})export class CourseCardComponent  {
  constructor(private storageService: StorageService){... }... }// storage.service.ts
@Injectable({ providedIn: 'root' })
export class StorageService {}

// user.module.ts
@NgModule({
  imports: [...]. .declarations: [...]. .providers: [StorageService]
})
export class UserModule {}Copy the code

Below, we will analyze the above shorthand mode.

In user.component.ts, we add the dependency Private storageService: storageService instead of the @Inject decorator, thanks to Angular’s InjectionToken design.

The InjectionToken does not have to be an InjectionToken object, as long as it recognizes the corresponding unique dependency in the runtime environment. So, in this case, you can use the class name, which is the name of the constructor in the runtime, as the InjectionToken for the dependency. Omit the step of creating the InjectionToken.

// user.module.ts
@NgModule({
  imports: [...]. .declarations: [...]. .providers: [{
    provide: StorageService, // Use the constructor name as the InjectionToken
    useFactory: storageServiceProviderFactory,
    deps] : []}})export class UserModule {}Copy the code

Note: Since Angular’s dependency injection system recognizes dependencies based on the InjectionToken in the runtime environment, it does dependency injection. The interface name cannot be used as an InjectionToken because it only exists at compile time in the Typescript language, not at runtime. For class names, which are represented in the runtime environment as constructor names, you can use them.

Next, we can replace the useFactory with useClass, which can actually create an instance as follows:

.providers: [{
  provide: StorageService,
  useClass: StorageService,
  deps: []}]...Copy the code

When using useClass, Angular treats the following value as a constructor and instantiates it in the runtime by executing the new directive, without having to create the Provider manually

Of course, Angular’s dependency injection design could have been simpler

.providers: [StorageService]
...
Copy the code

Writing the class name directly into the providers array, Angular identifies it as a constructor, looks inside the function, creates a factory function to find dependencies in its constructor, and instantiates it

Another feature of useClass is that Angular uses its runtime InjectionToken to automatically find providers based on the dependency’s Typescript type definition. So, don’t use the @Inject decorator to tell Angular where to Inject

You can abbreviate it as follows

.Constructor (@inject (StorageService) private StorageService: StorageService)
  constructor(private storageService: StorageService){... }...Copy the code

And that’s the most common way to write it in your development.

3. UseValue value provider

After completing the implementation of the local StorageService, we received a new requirement. The r&d boss wanted to provide a configuration file to store some default behaviors of the StorageService

Let’s start by creating a configuration

const storageConfig = {
  suffix: 'app_' // Add a prefix to store the key
  expires: 24 * 3600 * 100 // Expiration time, millisecond stamp
}
Copy the code

If you want to inject this configuration object into Angular, obviously useFactory and useClass are not suitable.

UseValue covers this scenario. It can be a normal variable or it can be an object.

The configuration method is as follows:

// config.ts
export interface STORAGE_CONFIG = {
  suffix: string;
  expires: number;
}

export const STORAGE_CONFIG_TOKEN = new InjectionToken<STORAGE_CONFIG>('storage-config');
export const storageConfig = {
  suffix: 'app_' // Add a prefix to store the key
  expires: 24 * 3600 * 100 // Expiration time, millisecond stamp
}

// user.module.ts
@NgModule({...providers: [
    StorageService,
    {
      provide: STORAGE_CONFIG_TOKEN,
      useValue: storageConfig
    }
  ],
  ...
})
export class UserModule {}
Copy the code

In the user.component.ts component, use the configuration object directly:

// user.component.ts
@Component({
  selector: 'app-user'.templateUrl: './user.component.html'.styleUrls: ['./user.component.css']})export class CourseCardComponent  {
  constructor(private storageService: StorageService, @Inject(STORAGE_CONFIG_TOKEN) private storageConfig: StorageConfig){... } getKey():void {
    const { suffix } = this.storageConfig;
    console.log(this.storageService.get(suffix + 'demo')); }}Copy the code

UseExisting Alias provider

The useExisting property is used if we need to create a new provider based on an existing provider, or if we need to rename an existing provider. For example, create an Angular form control that contains multiple forms, each of which stores different values. We can create it based on an existing form control provider

// new-input.component.ts
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'new-input'.exportAs: 'newInput'.providers: [{provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() = > NewInputComponent), // The NewInputComponent here is declared, but not yet defined. Instead of using the forwardRef directly, you can create an indirect reference that Angular will later parse
      multi: true}]})export class NewInputComponent implements ControlValueAccessor {... }Copy the code

ModuleInjector and ElementInjector level Injector

There are two injector hierarchies in Angular

  • ModuleInjector — Injected into modules using @NgModule() or @Injectable()

  • ElementInjector — Configured in the providers property of @Directive() or @Component()

We illustrate the application scenario of the two injectors with a practical example, such as designing a card component that displays user information

1. ModuleInjector ModuleInjector

We use user-card.component.ts to display the component and UserService to access the user’s information

// user-card.component.ts
@Component({
  selector: 'user-card.component.ts'.templateUrl: './user-card.component.html'.styleUrls: ['./user-card.component.less']})export class UserCardComponent {... }// user.service.ts
@Injectable({
  providedIn: "root"
})
export class UserService {... }Copy the code

The code was added to the root module via @Injectable (root is the alias of the root module). This is equivalent to the following code

// user.service.ts
export class UserService {... }// app.module.ts
@NgModule({...providers: [UserService], // Add via providers
})
export class AppModule {}
Copy the code

Of course, if you think UserService will only be used under the UserModule module, you don’t need to add it to the root module

// user.service.ts
@Injectable({
  providedIn: UserModule
})
export class UserService {... }Copy the code

Providers can be defined in the @Injectable({provideIn: XXX}) service file, or in the @NgModule({providers: [XXX]}) definition. So, what’s the difference?

@Injectable() and @NgModule() differ in how they are used.

@Injectable() ‘s providedIn property is better than @NgModule()’ s providers array because @Injectable() ‘s providedIn property provides providers with the providers array. Optimization tools can Tree Shaking to remove unused services from your application to reduce bundle size.

Let’s use an example to illustrate the above overview. As the business grew, we extended the services UserService1 and UserService2, but UserService2 has not been used for some reason.

To import dependencies via @NgModule() providers, we need to import user1.service.ts and user2.service.ts in user.module.ts. Then add UserService1 and UserService2 references to the providers array. Because UserService2 is referenced in the Module file, the Angular tree Shaker mistakenly thinks UserService2 is already in use. Tree shaking optimization cannot be performed. A code example is as follows:

// user.module.ts
import UserService1 from './user1.service.ts';
import UserService2 from './user2.service.ts';
@NgModule({...providers: [UserService1, UserService2], // Add via providers
})
export class UserModule {}
Copy the code

So, by using @Injectable({providedIn: UserModule}), we actually referenced use.module.ts in the service class itself and defined a provider for it. There is no need to repeat the definition in the UserModule, so there is no need to import the user2.service.ts file. So, UserService2 can be optimized away when it is not dependent. A code example is as follows:

// user2.service.ts
import UserModule from './user.module.ts';
@Injectable({
  providedIn: UserModule
})
export class UserService2 {... }Copy the code

2. ElementInjector Component Injector

Having learned about the ModuleInjector, let’s continue with the ElementInjector example.

Initially, our system had only one user, and we only needed a component and a UserService to access the user’s information

// user-card.component.ts
@Component({
  selector: 'user-card.component.ts'.templateUrl: './user-card.component.html'.styleUrls: ['./user-card.component.less']})export class UserCardComponent {... }// user.service.ts
@Injectable({
  providedIn: "root"
})
export class UserService {... }Copy the code

Note: The code above adds UserService to the root module, which will only be instantiated once.

If there are multiple users in the system at this time, the UserService in each user card component needs to access the corresponding user’s information. UserService generates only one instance if the method is followed. So it’s possible that After Joe saves the data, Joe goes to get the data, and he gets Joe’s results.

Is there a way to instantiate multiple UserServices so that each user’s data access operations are isolated?

The answer is yes. We need to use ElementInjector in the user.component.ts file. Add the Provider for UserService. As follows:

// user-card.component.ts
@Component({
  selector: 'user-card.component.ts'.templateUrl: './user-card.component.html'.styleUrls: ['./user-card.component.less'].providers: [UserService]
})
export class UserCardComponent {... }Copy the code

With the code above, each user card component instantiates a UserService to access its own user information.

To explain this, we need to mention Angular Components and Module Hierarchical Dependency Injection.

When using a dependency in a component, Angular looks through the component’s providers first to see if the dependency has a matching provider. If so, instantiate it directly. If not, look for the parent component’s providers. If not, look for the parent component’s parent until you reach the root component (app.component.ts). If a matching provider is found in the root component, it checks whether it has an existing instance, and if so, returns the instance directly. If not, the instantiation is performed. If the root component is still not found, the search starts in the module where the original component is. If the module where the original component is not found, the search continues to the parent Module until the root module (app.module.ts). If No provider for XXX is found, an error is reported.

@optional (), @self (), @skipself (), @host ()

When a dependency looks for a provider in An Angular application, you can use modifiers to make the search result fault-tolerant or limit the scope of the search.

1. @Optional()

Make the service Optional by decorating it with @optional (). That is, if No provider matching the service is found in the program, the program will not crash and an error message is displayed indicating No Provider for XXX, but null is returned.

export class UserCardComponent {
  constructor(@Optional private userService: UserService){}}Copy the code

2. @Self()

Use @self () to let Angular only look at the ElementInjector for the current component or directive.

Below, Angular will only search for a matching provider in the providers of the current UserCardComponent, and will report an error if there is no matching provider. No provider for UserService.

// user-card.component.ts
@Component({
  selector: 'user-card.component.ts'.templateUrl: './user-card.component.html'.styleUrls: ['./user-card.component.less'].providers: [UserService],
})
export class UserCardComponent {
  constructor(@Self(a)privateuserService? : UserService){}}Copy the code

3. @SkipSelf()

@skipself () is the opposite of @self (). Using @skipself (), Angular starts searching for services in the parent ElementInjector instead of the current ElementInjector.

// Child component user-card.component.ts
@Component({
  selector: 'user-card.component.ts'.templateUrl: './user-card.component.html'.styleUrls: ['./user-card.component.less'].providers: [UserService], // not work
})
export class UserCardComponent {
  constructor(@SkipSelf(a)privateuserService? : UserService){}}// Parent component mapping card.component.ts
@Component({
  selector: 'parent-card.component.ts'.templateUrl: './parent-card.component.html'.styleUrls: ['./parent-card.component.less'].providers: [{provide: UserService,
      useClass: ParentUserService, // work},]})export class ParentCardComponent {
  constructor(){}}Copy the code

4. @Host()

@host () allows you to specify the current component as the last stop in the injector tree when searching for providers. Similar to @self (), Angular doesn’t look for a service instance even if there is one higher up the tree.

4. Multi service providers

In some scenarios, we need an InjectionToken to initialize multiple providers. For example, when using an interceptor, we want to add a JWTInterceptor for token verification before default.interceptor.ts

.const NET_PROVIDES = [
  { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true}]; .Copy the code

Multi: If the value is false, the value of the provider will be overwritten. Set to true to generate multiple providers associated with a unique InjectionToken HTTP_INTERCEPTORS. Finally, you can get the values of all the providers through HTTP_INTERCEPTORS

Related articles

Understand Angular dependency Injection part 1: What is Dependency injection

Refer to the link

Angular Dependency Injection: Complete Guide

Dependency injection in Angular