Angular Injection Token use cases
In this post we'll explore Angular Injection Tokens, what they are and why you might want to use them.
Understanding Injection Tokens will be useful to help you gain a deeper understanding of Angular Dependency Injection. That said, you shouldn't use Injection Tokens unless they are solving a real issue in your application that can't be solved with a simpler solution.
What are Injection Tokens?
Injection Tokens are used with Angular Dependency Injection (DI) and hence we must understand a bit about Angular Dependency Injection. Dependency Injection is a fancy term for a relatively simple concept. You have some things (dependencies), which are stored somewhere (dependency pool) and you want something to give them to you when you need them (the Injector). Let's use a simple analogy to try and clarify what Dependency Injection is:
Lets say you have gone to a fancy restaurant with valet parking. You hand your keys to a valet and they give you a ticket. When you want your car back you hand your ticket to the valet and they return your car to you.
Your valet ticket is the Token that identifies the dependency you want, your car.
The restaurant car park contains all the cars, it is the dependency pool.
The valet is the Injector. You give the valet your ticket and the valet gets your car, you don't get multiple cars, a new car or cars belonging to other diners (hopefully).
An Angular Injection Token is no different. You create an Injection Token by calling new InjectionToken('some description string'), this returns a reference to your token. When you want to use the token you can use @Inject
or the Injector
to get it.
To make Angular aware of the dependency associated with the token you must add it as a provider in your application. This step is critical as if you don't do it you'll get an error as Angular doesn't know what to do. It's like the valet giving you a ticket and then forgetting to park your car or taking if for a spin!!
Creating a Simple Injection Token
Create a file called tokens.ts and add the following line to declare your first Injection Token:
export const MY_TOKEN = new InjectionToken<string>('myToken');
We now have MY_TOKEN Injection Token created. The 'myToken' can be anything it's just there as a description of the token. To use this Injection Token, we need to add it to the constructor of a Service or Component.
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(
@Inject(MY_TOKEN) private myToken: string
) {
console.log("Injected myToken", myToken);
}
}
If you run ng serve
you should see an error:
This is because we have created an Injection Token, so we have our valet ticket but when the valet tries to find our car it's not there. We are using it before Angular knows what it is and how to find it. We need to let Angular know what it needs to provide for this Injection Token. We can do that in one of three ways:
- by adding a factory function on the Injection Token definition (Will be available on Root Module Injector).
- by adding it to the providers array of the module that will use the Injection Token (Will be available on Root Module Injector)
- by adding it to the providers array of the component that will use the Injection Token (Will be available on Element Injector)
An example of each is shown below. Which too choose depends on several factors, which are beyond the scope of this post, if in doubt use the factory function on the provider itself.
factory
is a function that has a return type of unknown
, the type will be inferred once you provide a return value. In the above example the type is inferred as string
but we could return an Array, Object or Class.
Now the error should be gone as we have told Angular about our Token. Notice we have linked MY_TOKEN
with a string value. We now have our first working token being injected into our application. However, injecting a string value may not seem very useful, as we could do the same by using an environment file and a lot less code. We'll explore other options below.
There is another optional parameter that can be used when creating an Injection TokenprovidedIn
, this will default toroot
if not explicitly added. The other options areany
orplatform
.
When adding a token to the module or component providers array there are 4 options:
- useValue
- useFactory
- useClass
- useExisting
useValue
We have already used useValue to provide a string. We could also provide an array or an object using useValue.
useClass
Not much mystery here, we can use our Injection Token to provide a class instance. As the token is provided on the app.module.ts there will only be one instance of the class created, no matter how many times it is injected. Also, note that we don't need to specify useClass
if it is a class being provided, it will be inferred.
export class Token {
constructor() {
console.log("Token Class");
}
}
// app.module.ts
@NgModule({
..
providers:
[{
provide: MY_TOKEN,
useClass: Token
}]
})
// Sugur syntax when using Class
// app.module.ts
@NgModule({
..
providers: [MY_TOKEN]
})
useFactory
This is where things get interesting. You can provide a factory function that will determine what needs to be returned based on the context of the request.
export function TokenFactory() {
// perform config lookup and return based on context
}
// app.module.ts
@NgModule({
..
providers:
[{
provide: MY_TOKEN,
useFactory: TokenFactory
}]
})
You may be wondering how this works with services. Any services that are annotated with @Injectable
are already provided and available for use in your component, directives or pipes so there is no need to create Injection Tokens for them.
useExisting
With useExisting
angular returns the item associated with the Aliased Token, MY_TOKEN_ALIAS
. If the aliased token does not exist you will get an error.
// app.module.ts
@NgModule({
..
providers:
[{
provide: MY_TOKEN_ALIAS,
useExisting: MY_TOKEN
}]
})
useExisting
can be used when a token has already been resolved and you want to provide a narrower API surface to the one exposed by the existing provider, which will typically be a service. For large applications with many shared services this may prove useful where a child component only needs a subset of a services functionality.
Use BehaviorSubject with InjectionToken
An interesting use of Injection Tokens would be to use a BehaviorSubject to communicate between components.
A note on types when using InjectionToken
When using @Inject(SomeToken)
the type is not inferred. So if you define an InjectionToken that returns a number and add an incorrect type in the constructor you won't get an error. If you use the component Injector to get the token the type will be inferred correctly.
export const MY_TOKEN = new InjectionToken<string>('MyToken', {
factory: () => 'Some Value'
});
constructor(
// no type error even though token is a string
@Inject(MY_TOKEN) token: number
) {
}
constructor(
private injector: Injector,
) {
// is inferred correctly as string
const token = injector.get(MY_TOKEN);
}
Wrap Up
Hopefully this has helped demystify Injection Tokens. They are a useful tool in the tool belt but should be used sparingly and only where they are adding value and not increasing the complexity unnecessarily.