Skip to main content

Angular Injection Tokens

Introduction

In Angular's dependency injection system, we typically inject services as classes. However, there are situations where you need to inject values that aren't classes, such as strings, configurations, or objects. This is where Injection Tokens come into play.

Injection tokens provide a way to create unique identifiers that can be used with Angular's dependency injection system to inject non-class dependencies. They help Angular distinguish between different values of the same primitive type or interface.

Understanding Injection Tokens

What is an Injection Token?

An Injection Token is essentially a unique identifier that can be used to associate a particular value with a token in Angular's dependency injection system. It's represented by the InjectionToken class in Angular.

Let's create a simple injection token:

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

export const API_URL = new InjectionToken<string>('api.url');

The generic type parameter (<string> in this example) indicates the type of value that will be associated with this token. The string parameter ('api.url') is a description that helps with debugging.

Creating and Using Injection Tokens

Basic Usage

Here's a step-by-step example of how to create and use an injection token:

  1. First, create the token:
typescript
// tokens.ts
import { InjectionToken } from '@angular/core';

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

export interface AppConfig {
apiUrl: string;
theme: string;
debugMode: boolean;
}
  1. Provide a value for the token in a module:
typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { APP_CONFIG } from './tokens';

@NgModule({
providers: [
{
provide: APP_CONFIG,
useValue: {
apiUrl: 'https://api.example.com',
theme: 'light',
debugMode: false
}
}
]
})
export class AppModule { }
  1. Inject and use the token in a component or service:
typescript
// data.service.ts
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { APP_CONFIG, AppConfig } from './tokens';

@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(
private http: HttpClient,
@Inject(APP_CONFIG) private config: AppConfig
) { }

getData() {
// Use the injected configuration
return this.http.get(`${this.config.apiUrl}/data`);
}
}

Using Injection Tokens with Factory Functions

Sometimes you need to compute a value dynamically. You can use factory functions with injection tokens:

typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { APP_CONFIG } from './tokens';
import { environment } from '../environments/environment';

@NgModule({
providers: [
{
provide: APP_CONFIG,
useFactory: () => {
return {
apiUrl: environment.production
? 'https://api.example.com'
: 'http://localhost:3000',
theme: 'light',
debugMode: !environment.production
};
}
}
]
})
export class AppModule { }

Real-World Applications

Application Configuration

One common use case for injection tokens is providing application-wide configuration:

typescript
// config.ts
import { InjectionToken } from '@angular/core';

export interface AppConfig {
apiUrl: string;
pageSize: number;
featureFlags: {
newUserInterface: boolean;
betaFeatures: boolean;
};
}

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { APP_CONFIG, AppConfig } from './config';
import { environment } from '../environments/environment';

const appConfig: AppConfig = {
apiUrl: environment.apiUrl,
pageSize: 10,
featureFlags: {
newUserInterface: environment.production ? false : true,
betaFeatures: environment.enableBeta
}
};

@NgModule({
providers: [
{ provide: APP_CONFIG, useValue: appConfig }
]
})
export class AppModule { }

Multi-Provider Tokens

Injection tokens can be used with "multi" providers, which allows multiple values to be injected for a single token:

typescript
// plugin.token.ts
import { InjectionToken } from '@angular/core';

export interface Plugin {
name: string;
execute(): void;
}

export const PLUGINS = new InjectionToken<Plugin[]>('app.plugins');
typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { PLUGINS, Plugin } from './plugin.token';

export class LoggerPlugin implements Plugin {
name = 'Logger';
execute() {
console.log('Logger plugin executed');
}
}

export class AnalyticsPlugin implements Plugin {
name = 'Analytics';
execute() {
console.log('Analytics tracking started');
}
}

@NgModule({
providers: [
{ provide: PLUGINS, useClass: LoggerPlugin, multi: true },
{ provide: PLUGINS, useClass: AnalyticsPlugin, multi: true }
]
})
export class AppModule { }
typescript
// plugin.service.ts
import { Injectable, Inject } from '@angular/core';
import { PLUGINS, Plugin } from './plugin.token';

@Injectable({
providedIn: 'root'
})
export class PluginService {
constructor(@Inject(PLUGINS) private plugins: Plugin[]) { }

executeAll() {
this.plugins.forEach(plugin => {
console.log(`Executing plugin: ${plugin.name}`);
plugin.execute();
});
}
}

Injection Token with Default Value

You can provide a default value for an injection token:

typescript
// tokens.ts
import { InjectionToken } from '@angular/core';

export const API_TIMEOUT = new InjectionToken<number>('api.timeout', {
providedIn: 'root',
factory: () => 3000 // Default timeout of 3 seconds
});

This way, even if no provider explicitly sets this token, it will have a default value.

Best Practices for Using Injection Tokens

  1. Use descriptive names: Choose token names that clearly indicate their purpose.
  2. Use interfaces for complex values: Define TypeScript interfaces for complex token values.
  3. Group related tokens: Keep related tokens together in the same file.
  4. Document your tokens: Add comments or documentation about expected values and usage.
  5. Consider using default values: For non-critical configuration, provide sensible defaults.

Common Pitfalls and How to Avoid Them

Forgetting to Import the Token

Make sure you import the token from the correct location:

typescript
// Correct way:
import { MY_TOKEN } from './tokens';

// Incorrect: creating a new token with the same name
const MY_TOKEN = new InjectionToken<string>('my.token'); // DON'T DO THIS!

Not Using @Inject Decorator

When injecting tokens that aren't class types, you must use the @Inject decorator:

typescript
// Correct:
constructor(@Inject(API_URL) private apiUrl: string) { }

// Incorrect:
constructor(private apiUrl: API_URL) { } // This won't work as expected

Summary

Angular Injection Tokens provide a powerful way to work with non-class dependencies in Angular's dependency injection system. They allow you to:

  • Inject primitive values like strings and numbers
  • Provide application configuration
  • Create plugin systems with multi-providers
  • Set up environment-specific values

By understanding and using injection tokens effectively, you can create more flexible, configurable, and maintainable Angular applications.

Additional Resources

Exercises

  1. Create an injection token for application theme settings and provide it in your app module.
  2. Implement a multi-provider for a notification system that supports different notification methods (console, toast, email).
  3. Build a feature flag system using injection tokens that can enable/disable features based on the environment.
  4. Create an injection token with a factory function that provides different values based on the current user's role.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)