Stop being scared of InjectionTokens

achtlos

thomas

Posted on February 2, 2023

Stop being scared of InjectionTokens

Injection Tokens seem to intimidate many developers. They don't understand what they are, how to use them, and their purpose.

To overcome the fear of this feature, it is important to have a basic understanding of how Angular's Dependency Injection works. (A separate article will dive deeper into the inner workings of Angular's DI system for a full understanding).

Here's an example of how the singleton pattern operates. When we add @Injectable({providedIn: 'root'}) to one of our services:

@Injectable({ providedIn: 'root' })
export class MyService {
 // ...
}
Enter fullscreen mode Exit fullscreen mode

The first time, we will call MyService Angular will store the service in a record of the RootInjector with the key being class MyService and the value being an object containing a factory of MyService , the current value and a multi flag.

record:{
 //...
 [index]:{
   key: class MyService,
   value: {
    factory: f MyService_Factory(t),
    multi: undefined
    value: {}
   }
 //...
}
Enter fullscreen mode Exit fullscreen mode

This way, the next time we want to inject our service through the constructor or the inject function in another Component, Angular's DI will look for the service in the record object and return its current value or create it using the factory function.


It's also crucial to understand what happens when we provide our service directly in the bootstrapApplication or Component provider array.

To inject MyService we write the following:

@Component({
  // ...
  providers: [MyService],
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

This code is a shorthand syntax for

@Component({
  // ...
  providers: [{provide: MyService, useClass: MyService}],
})
export class AppComponent {}
Enter fullscreen mode Exit fullscreen mode

By creating an InjectionToken, we are creating a "static" key for our record or dictionary of services.

By writing this:

export const DATA = new InjectionToken<string>('data');
Enter fullscreen mode Exit fullscreen mode

We are creating a "constant" of type string.

The InjectionToken can take an object as second parameter containing a factory function and a providedIn attribut (Although this parameter will be deprecated as the default value is root and there is no other possibility). The factory function acts like a default value if no other value is provided.

Let's take a look at the following example to clarify it.

export const DATA = new InjectionToken<string>('data', {
  factory: () => 'toto',
});
Enter fullscreen mode Exit fullscreen mode

Angular will store our token inside the record object of the RootInjector:

record: {
 //...
 [index]: {
   key: InjectionToken {_desc: 'data', ngMetadataName: 'InjectionToken'}
   value: {
     factory: () => 'toto',
     multi: undefined,
     value: "toto"
   }
 },
 //...
}
Enter fullscreen mode Exit fullscreen mode

Then when we inject it inside a Component, Angular's DI will look for DATA key in the RootInjector's record and inject the corresponding value. ('toto' is this example)

@Component({
  // ...
  template: `{{ data }} `, // toto
})
export class AppComponent {
  data = inject(DATA);
}
Enter fullscreen mode Exit fullscreen mode

However, in a Component provider array, we can override this Token to map DATA with another value. This time Angular will store the value inside the record of the NodeInjector.

@Component({
  // ...
  provider: [{provide: DATA, useValue: 'titi'}],
  template: `{{ data }} `, // titi
})
export class AppComponent {
  data = inject(DATA);
}
Enter fullscreen mode Exit fullscreen mode

When Angular searches for the DATA token, it first examines the component's NodeInjector, then the NodeInjector of its parent and finally the RootInjector. As a result, in this example, the output value will be 'titi'.

Providing your InjectionToken

useValue

The useValue keyword allows us to supply a string, number, interface, class instance or any constant value. It is very useful for setting up configuration properties.

export type Environment = 'local' | 'dev' | 'int' | 'prod';

export interface AppConfig {
  version: string;
  apiUrl: string;
  environment: Environment;
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

export const getAppConfigProvider = (value: AppConfig): ValueProvider => ({
  provide: APP_CONFIG,
  useValue: value,
});

bootstrapApplication(AppComponent, {
  providers: [
    getAppConfigProvider(environment) // environment files
  ],
});
Enter fullscreen mode Exit fullscreen mode

In the example above, we are creating an InjectionToken to store our environment configuration. 

This is the recommended method for accessing environment variables in an Nx workspace, as it avoids circular dependencies.
To retrieve our environment properties, we inject the token inside our component.

@Component({
  selector: 'app-root',
  standalone: true,
  template: `{{ config.version }}`,
})
export class AppComponent {
  config = inject(APP_CONFIG);
}
Enter fullscreen mode Exit fullscreen mode

useClass

The useClass keyword allows us to instantiate a new class. This is useful for implementing the dependency inversion principle for instance.

The dependency inversion principle is a software design principle that states that different layers depend on abstractions, rather than on each other. This inversion makes the code more flexible, maintainable and more testable.

export interface Search<T> {
  search: (search: string) => T[];
}

export const SEARCH = new InjectionToken<Search<object>>('search');

@Component({
  selector: 'shareable',
  standalone: true,
  imports: [ReactiveFormsModule, NgFor, AsyncPipe, JsonPipe],
  template: `
    <input type="text" [formControl]="searchInput" />
    <button (click)="search()"></button>
    <div *ngFor="let d of data | async">{{ d | json }}</div>
  `,
})
export class ShareableComponent {
  searchService = inject(SEARCH, {optional: true}); 
  data = new BehaviorSubject<object[]>([]);

  searchInput = new FormControl('', { nonNullable: true });

  // We are not injecting `searchService` with the constructor, 
  // because `inject` function infers the type. 
  constructor() {
    if (!this.searchService) 
       throw new Error(`SEARCH TOKEN must be PROVIDED`);
  }

  search() {
    this.data.next(this.searchService.search(this.searchInput.value));
  }
}
Enter fullscreen mode Exit fullscreen mode

The ShareableComponent displays an input field, a search button and a list of the search results. When we hit the search button, the component searches "somewhere" and displays the results. This component is highly generic and does not require any additional information about how or where to search. Implementation details are provided by the parent component.

Let's see how we can utilize the ShareableComponent.

First, we will create an utility function to provide our Token. This creates a better developer experience and reduces the likelihood of errors as it adds strong typing to our function.

export const getSearchServiceProvider = <T, C extends Search<T>>(clazz: new () => C): ClassProvider => ({
  provide: SEARCH,
  useClass: clazz,
});
Enter fullscreen mode Exit fullscreen mode

Using the above function, we can supply the desire implementation of the Search interface within the component that calls the ShareableComponent. When Angular attempts to inject the SEARCH InjectionToken , it will traverse each NodeInjector up the component tree until it finds the provided implementation.

 

@Injectable()
export class DetailSearchService implements Search<Detail> {
  search = (search: string): Detail[] => {
  // implementation of our search function
  }
}

@Component({
  selector: 'parent',
  standalone: true,
  imports: [ShareableComponent],
  providers: [getSearchServiceProvider(DetailSearchService)],
  template: `
   <shareable></shareable>
  `,
})
export class ParentComponent {}
Enter fullscreen mode Exit fullscreen mode

If we want to reuse ShareableComponent with a different implementation of search, this becomes very easy. 

useFactory

The useFactory keyword allows us to provide an object though a factory function.

export const USER = new InjectionToken<string>('user');

export const getUserProvider = (index: number): FactoryProvider => ({
  provide: USER,
  useFactory: () => inject(Store).select(selectUser(index)),
});
Enter fullscreen mode Exit fullscreen mode

The inject function enables us to incorporate other services within the factory function.

Alternatively, if you do not wish to use the inject function, the FactoryProvider has a third parameter named deps where you can inject other injectable services.

export const getUserProvider = (index: number): FactoryProvider => ({
  provide: USER,
  useFactory: (store: Store) => store.select(selectUser(index))],
  deps: [Store],
});
Enter fullscreen mode Exit fullscreen mode

But I advice you to use the inject function. It's cleaner and easier to understand.

We can then use this provider as follow to retrieve one user from the store:

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [getUserProvider(2)],
  template: ` <div>{{ user }}</div> `,
})
export class ParentComponent {
  user = inject(USER);
}
Enter fullscreen mode Exit fullscreen mode

useExisting

The useExisting keyword allows us to map an existing instance of a service to a new token, creating an alias.

When examining the record object created by Angular, we have the following:

 

record:{
 //...
 [index]:{
   key: InjectionToken {_desc: 'data', ngMetadataName: 'InjectionToken', ɵprov: undefined},
   value: {
    factory: () => ɵɵinject(resolveForwardRef(provider.useExisting)),
    multi: undefined
    value: {}
   }
 //...
}
Enter fullscreen mode Exit fullscreen mode

Angular resolves the InjectionToken by forwarding the reference to the class specified in the useExisting parameter. It searches the NodeInjector and RootInjector for the class, so it must be set beforehand or an error will occur.

NullInjectorError: No provider for XXX!
Enter fullscreen mode Exit fullscreen mode

Notes

When providing an InjectionToken through the providers, there is not type safety. For example, the following code would not produce a Typescript error, even though the type is inferred as number for num , but a string is provided. (We might have an error at runtime.)
export const NUMBER = new InjectionToken('number');

@Component({
  providers: [{provide: NUMBER, useValue: 'toto'}],
  // ... 
})
export class ParentComponent {
  num = inject(NUMBER);
  //^ (property) ParentComponent.num: number
}
Enter fullscreen mode Exit fullscreen mode

To ensure type safety, I advise to create a utility function to provide your token.

export const NUMBER = new InjectionToken<number>('number');

export const getNumberProvider = (num: number): ValueProvider => ({
  provide: NUMBER,
  useValue: num
});
// NUMBER token should be provided through this function

@Component({
  providers: [getNumberProvider('toto')],
                                 //^ Argument of type 'string' is not assignable to parameter of type 'number'
  // ...
})
export class ParentComponent {
  num = inject(NUMBER);
}
Enter fullscreen mode Exit fullscreen mode

This way, our implementation is safe and any errors will occur at compile time, rather than at runtime.


That's it for this article! You should now have a good understanding of InjectionToken and what they do mean. I hope that this information has demystified them for you and you will no longer feel intimidated by them.

I hope you learned new Angular concept. If you liked it, you can find me on Twitter or Github.

👉 If you want to accelerate your Angular and Nx learning journey, come and check out Angular challenges.

💖 💪 🙅 🚩
achtlos
thomas

Posted on February 2, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Angular Form Array
angular Angular Form Array

November 29, 2024

Can a Solo Developer Build a SaaS App?
undefined Can a Solo Developer Build a SaaS App?

November 29, 2024

Angular's New Feature: Signals
javascript Angular's New Feature: Signals

November 29, 2024