home / skills / analogjs / angular-skills / angular-di

angular-di skill

/skills/angular-di

This skill helps Angular developers implement and configure dependency injection with inject(), providers, and tokens to manage service lifetimes and

npx playbooks add skill analogjs/angular-skills --skill angular-di

Review the files below or copy the command above to add this skill to your agents.

Files (2)
SKILL.md
8.3 KB
---
name: angular-di
description: Implement dependency injection in Angular v20+ using inject(), injection tokens, and provider configuration. Use for service architecture, providing dependencies at different levels, creating injectable tokens, and managing singleton vs scoped services. Triggers on service creation, configuring providers, using injection tokens, or understanding DI hierarchy.
---

# Angular Dependency Injection

Configure and use dependency injection in Angular v20+ with `inject()` and providers.

## Basic Injection

### Using inject()

Prefer `inject()` over constructor injection:

```typescript
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  template: `...`,
})
export class UserListComponent {
  // Inject dependencies
  private http = inject(HttpClient);
  private userService = inject(UserService);
  
  // Can use immediately
  users = this.userService.getUsers();
}
```

### Injectable Services

```typescript
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root', // Singleton at root level
})
export class UserService {
  private http = inject(HttpClient);
  
  private users = signal<User[]>([]);
  readonly users$ = this.users.asReadonly();
  
  async loadUsers() {
    const users = await firstValueFrom(
      this.http.get<User[]>('/api/users')
    );
    this.users.set(users);
  }
}
```

## Provider Scopes

### Root Level (Singleton)

```typescript
// Recommended: providedIn
@Injectable({
  providedIn: 'root',
})
export class AuthService {}

// Alternative: in app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    AuthService,
  ],
};
```

### Component Level (Instance per Component)

```typescript
@Component({
  selector: 'app-editor',
  providers: [EditorStateService], // New instance for each component
  template: `...`,
})
export class EditorComponent {
  private editorState = inject(EditorStateService);
}
```

### Route Level

```typescript
export const routes: Routes = [
  {
    path: 'admin',
    providers: [AdminService], // Shared within this route tree
    children: [
      { path: '', component: AdminDashboardComponent },
      { path: 'users', component: AdminUsersComponent },
    ],
  },
];
```

## Injection Tokens

### Creating Tokens

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

// Simple value token
export const API_URL = new InjectionToken<string>('API_URL');

// Object token
export interface AppConfig {
  apiUrl: string;
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

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

// Token with factory (self-providing)
export const WINDOW = new InjectionToken<Window>('Window', {
  providedIn: 'root',
  factory: () => window,
});

export const LOCAL_STORAGE = new InjectionToken<Storage>('LocalStorage', {
  providedIn: 'root',
  factory: () => localStorage,
});
```

### Providing Token Values

```typescript
// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    { provide: API_URL, useValue: 'https://api.example.com' },
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        features: { darkMode: true, analytics: true },
      },
    },
  ],
};
```

### Injecting Tokens

```typescript
@Injectable({ providedIn: 'root' })
export class ApiService {
  private apiUrl = inject(API_URL);
  private config = inject(APP_CONFIG);
  private window = inject(WINDOW);
  
  getBaseUrl(): string {
    return this.apiUrl;
  }
}
```

## Provider Types

### useClass

```typescript
// Provide implementation
{ provide: LoggerService, useClass: ConsoleLoggerService }

// Conditional implementation
{
  provide: LoggerService,
  useClass: environment.production 
    ? ProductionLoggerService 
    : ConsoleLoggerService,
}
```

### useValue

```typescript
// Static values
{ provide: API_URL, useValue: 'https://api.example.com' }

// Configuration objects
{ provide: APP_CONFIG, useValue: { theme: 'dark', language: 'en' } }
```

### useFactory

```typescript
// Factory with dependencies
{
  provide: UserService,
  useFactory: (http: HttpClient, config: AppConfig) => {
    return new UserService(http, config.apiUrl);
  },
  deps: [HttpClient, APP_CONFIG],
}

// Async factory (not recommended - use APP_INITIALIZER)
{
  provide: CONFIG,
  useFactory: () => fetch('/config.json').then(r => r.json()),
}
```

### useExisting

```typescript
// Alias to existing provider
{ provide: AbstractLogger, useExisting: ConsoleLoggerService }

// Multiple tokens pointing to same instance
providers: [
  ConsoleLoggerService,
  { provide: Logger, useExisting: ConsoleLoggerService },
  { provide: ErrorLogger, useExisting: ConsoleLoggerService },
]
```

## Injection Options

### Optional Injection

```typescript
@Component({...})
export class MyComponent {
  // Returns null if not provided
  private analytics = inject(AnalyticsService, { optional: true });
  
  trackEvent(name: string) {
    this.analytics?.track(name);
  }
}
```

### Self, SkipSelf, Host

```typescript
@Component({
  providers: [LocalService],
})
export class ParentComponent {
  // Only look in this component's injector
  private local = inject(LocalService, { self: true });
}

@Component({...})
export class ChildComponent {
  // Skip this component, look in parent
  private parentService = inject(ParentService, { skipSelf: true });
  
  // Only look up to host component
  private hostService = inject(HostService, { host: true });
}
```

## Multi Providers

Collect multiple values for same token:

```typescript
// Token for multiple validators
export const VALIDATORS = new InjectionToken<Validator[]>('Validators');

// Provide multiple values
providers: [
  { provide: VALIDATORS, useClass: RequiredValidator, multi: true },
  { provide: VALIDATORS, useClass: EmailValidator, multi: true },
  { provide: VALIDATORS, useClass: MinLengthValidator, multi: true },
]

// Inject as array
@Injectable()
export class ValidationService {
  private validators = inject(VALIDATORS); // Validator[]
  
  validate(value: string): ValidationError[] {
    return this.validators
      .map(v => v.validate(value))
      .filter(Boolean);
  }
}
```

### HTTP Interceptors (Multi Provider)

```typescript
// Interceptors use multi providers internally
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor,
        loggingInterceptor,
        errorInterceptor,
      ])
    ),
  ],
};
```

## APP_INITIALIZER

Run async code before app starts:

```typescript
import { APP_INITIALIZER } from '@angular/core';

function initializeApp(configService: ConfigService): () => Promise<void> {
  return () => configService.loadConfig();
}

export const appConfig: ApplicationConfig = {
  providers: [
    ConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeApp,
      deps: [ConfigService],
      multi: true,
    },
  ],
};
```

### Multiple Initializers

```typescript
providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: (config: ConfigService) => () => config.load(),
    deps: [ConfigService],
    multi: true,
  },
  {
    provide: APP_INITIALIZER,
    useFactory: (auth: AuthService) => () => auth.checkSession(),
    deps: [AuthService],
    multi: true,
  },
]
```

## Environment Injector

Create injectors programmatically:

```typescript
import { createEnvironmentInjector, EnvironmentInjector, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class PluginService {
  private parentInjector = inject(EnvironmentInjector);
  
  loadPlugin(providers: Provider[]): EnvironmentInjector {
    return createEnvironmentInjector(providers, this.parentInjector);
  }
}
```

## runInInjectionContext

Run code with injection context:

```typescript
import { runInInjectionContext, EnvironmentInjector, inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UtilityService {
  private injector = inject(EnvironmentInjector);
  
  executeWithDI<T>(fn: () => T): T {
    return runInInjectionContext(this.injector, fn);
  }
}

// Usage
utilityService.executeWithDI(() => {
  const http = inject(HttpClient);
  // Use http...
});
```

For advanced patterns, see [references/di-patterns.md](references/di-patterns.md).

Overview

This skill implements dependency injection patterns for Angular v20+ using inject(), InjectionToken, and provider configuration. It helps you design service architecture, control provider scope (root, component, route), and create configurable tokens and factories. Use it to manage singletons, scoped instances, multi providers, and advanced injector scenarios.

How this skill works

The skill inspects service and component creation flows, provider declarations, and token usage to recommend correct provider types (useClass, useValue, useFactory, useExisting). It highlights where to apply providedIn versus explicit providers, how to create and inject tokens, and when to use optional/self/skipSelf/host injection options. It also covers advanced APIs: multi providers, APP_INITIALIZER, environment injectors, and runInInjectionContext for runtime DI.

When to use it

  • Designing app-wide singletons or scoped services (root vs component vs route).
  • Creating configuration tokens or environment-specific values with InjectionToken.
  • Replacing implementations via useClass/useExisting for testing or platform variation.
  • Collecting plugins or validators with multi providers.
  • Running initialization tasks before app start with APP_INITIALIZER.

Best practices

  • Prefer inject() over constructor injection for concise, testable classes and factory functions.
  • Use providedIn: 'root' for true singletons; use component or route providers for per-instance state.
  • Prefer InjectionToken for typed, named configuration and self-providing tokens with factories for globals (window, localStorage).
  • Use useFactory with explicit deps for complex instantiation; avoid async factories—use APP_INITIALIZER instead.
  • Prefer multi providers for extensibility (interceptors, validators) and use useExisting to alias implementations without extra instances.

Example use cases

  • Provide an API base URL via an InjectionToken and swap values per environment using useValue.
  • Register a per-component EditorStateService to keep multiple editor instances isolated.
  • Expose multiple validation rules using a multi VALIDATORS token and aggregate them in a ValidationService.
  • Create a platform WINDOW token with a factory so services can access window safely and be testable.
  • Use APP_INITIALIZER to load remote configuration before boot and register services based on that config.

FAQ

When should I use providedIn vs adding a provider in ApplicationConfig?

Use providedIn for most singletons; it enables tree-shaking and simpler code. Add providers in ApplicationConfig when you need conditional registration, environment-specific overrides, or to provide values (useValue/useFactory) at app bootstrap.

Can I use async factories for providers?

Async factories are possible but not recommended; they delay injector resolution. Prefer APP_INITIALIZER to run async startup work and then register synchronous providers if needed.