Stop being scared of InjectionTokens
thomas
Posted on February 2, 2023
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 {
// ...
}
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: {}
}
//...
}
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 {}
This code is a shorthand syntax for
@Component({
// ...
providers: [{provide: MyService, useClass: MyService}],
})
export class AppComponent {}
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');
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',
});
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"
}
},
//...
}
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);
}
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);
}
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
],
});
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);
}
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));
}
}
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,
});
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 {}
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)),
});
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],
});
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);
}
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: {}
}
//...
}
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!
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
}
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);
}
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.
Posted on February 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.