Skip to main content
Technology & EngineeringAngular284 lines

Angular Dependency Injection

Angular dependency injection system including providers, injection tokens, and hierarchical injectors

Quick Summary30 lines
You are an expert in Angular's dependency injection system for building loosely coupled, testable Angular applications.

## Key Points

1. **Environment injectors** — root injector, module injectors, route-level injectors
2. **Element injectors** — component and directive injectors
1. **Default to `providedIn: 'root'` for application-wide singletons.** It is tree-shakable and requires no explicit provider registration.
2. **Use component-level providers for state that should be scoped.** Form state, editor state, and wizard state are good candidates for component-provided services.
3. **Prefer `inject()` over constructor parameters.** It is more concise, works in field initializers, and enables better refactoring.
4. **Use `InjectionToken` for configuration and non-class values.** Never use raw strings as tokens — they are not type-safe and collide easily.
5. **Design for testability.** Every service that depends on browser APIs (localStorage, fetch, window) should be behind an injectable abstraction so tests can substitute fakes.
6. **Avoid deep provider hierarchies.** If debugging "which instance am I getting?" becomes difficult, simplify the provider tree.
- **Circular dependencies.** Service A injects Service B which injects Service A. Angular throws a cyclic dependency error. Break the cycle by introducing a mediator service or using `forwardRef`.
- **Missing provider.** `NullInjectorError: No provider for X` means no injector in the chain has registered the dependency. Ensure the service is provided at the correct level.
- **Unintended singletons.** Providing a service at root when you need per-component instances means all components share the same state. Move the provider to the component level.
- **Forgetting `multi: true` on multi-providers.** Without `multi: true`, each provider registration replaces the previous one instead of appending.

## Quick Example

```typescript
private config = inject(APP_CONFIG);
```

```typescript
providers: [
  { provide: StorageService, useClass: LocalStorageService },
]
```
skilldb get angular-skills/Angular Dependency InjectionFull skill: 284 lines
Paste into your CLAUDE.md or agent config

Dependency Injection — Angular

You are an expert in Angular's dependency injection system for building loosely coupled, testable Angular applications.

Core Philosophy

Angular's dependency injection system is the architectural backbone that makes the framework's components testable, configurable, and loosely coupled. The core idea is inversion of control: a component declares what it needs (a service, a configuration token, an abstract interface) without knowing how to create it. The injector resolves the dependency at runtime based on the provider configuration. This indirection is not overhead — it is the mechanism that lets you swap real services for test doubles, provide different implementations in different parts of the application, and tree-shake unused services.

The hierarchical injector system is Angular's most powerful and most misunderstood feature. Injectors form a tree: the root injector provides application-wide singletons, route-level injectors scope services to feature areas, and component-level injectors create per-instance state. Understanding which injector provides a given service determines its lifetime and sharing scope. A service provided at root is a singleton. The same service provided in a component's providers array creates a new instance for each component. This is not a configuration detail — it is an architectural decision about state ownership.

Modern Angular strongly favors the inject() function over constructor injection. It is more concise, works in field initializers and factory functions, and enables better refactoring. Combined with InjectionToken for non-class values and providedIn: 'root' for tree-shakable singletons, the modern DI API removes most of the ceremony that historically made Angular's DI feel heavy.

Anti-Patterns

  • Circular Service Dependencies — service A injecting service B which injects service A, causing Angular to throw a cyclic dependency error. Break the cycle with a mediator service, lazy injection via Injector, or forwardRef.

  • Accidental Multiple Instances via useClass — writing { provide: A, useClass: B } when B is also providedIn: 'root', creating two separate instances of B. Use useExisting when you want to alias to the existing singleton.

  • Raw String Tokens — using plain strings as injection tokens (provide: 'apiUrl') instead of InjectionToken<string>. String tokens are not type-safe, easily collide across libraries, and produce confusing runtime errors.

  • Component-Level Providers for Intended Singletons — accidentally placing a service in a component's providers array when it should be a singleton, creating a new instance for every component instance and breaking shared state assumptions.

  • Forgetting multi: true on Multi-Providers — registering multiple providers for the same token without multi: true, causing each registration to silently replace the previous one instead of accumulating into an array.

Overview

Angular's dependency injection (DI) system is a design pattern and mechanism that allows classes to declare dependencies they need without creating them directly. Angular's DI is hierarchical — injectors form a tree that parallels the component tree, enabling precise control over service scope and lifetime.

Core Concepts

The inject() Function

The modern, preferred way to request dependencies in Angular:

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';

@Component({ /* ... */ })
export class ProductDetailComponent {
  private http = inject(HttpClient);
  private route = inject(ActivatedRoute);
}

inject() works in constructors, field initializers, and factory functions — anywhere an injection context is active.

providedIn: 'root'

The most common way to register a singleton service:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private token: string | null = null;

  login(username: string, password: string) { /* ... */ }
  logout() { this.token = null; }
  isAuthenticated(): boolean { return this.token !== null; }
}

Services with providedIn: 'root' are tree-shakable — they are only included in the bundle if actually injected somewhere.

InjectionToken

Use InjectionToken for non-class dependencies (primitives, interfaces, configuration objects):

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

export interface AppConfig {
  apiUrl: string;
  enableDebug: boolean;
  maxRetries: number;
}

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

Provide it during bootstrap:

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        enableDebug: false,
        maxRetries: 3,
      },
    },
  ],
};

Inject it:

private config = inject(APP_CONFIG);

Hierarchical Injectors

Angular has two injector hierarchies:

  1. Environment injectors — root injector, module injectors, route-level injectors
  2. Element injectors — component and directive injectors
// Scoped to a component and its children
@Component({
  selector: 'app-editor',
  providers: [EditorStateService],
  template: `...`,
})
export class EditorComponent {
  private state = inject(EditorStateService);
}

Each EditorComponent instance gets its own EditorStateService. Child components that inject the same service receive the parent's instance.

Implementation Patterns

Factory Providers

Use useFactory when creation logic is complex or conditional:

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: LoggerService,
      useFactory: () => {
        const config = inject(APP_CONFIG);
        return config.enableDebug
          ? new DebugLoggerService()
          : new ProductionLoggerService();
      },
    },
  ],
};

Abstract Class as Token

Use an abstract class as both a type and a DI token — avoids the need for InjectionToken:

export abstract class StorageService {
  abstract get(key: string): string | null;
  abstract set(key: string, value: string): void;
  abstract remove(key: string): void;
}

@Injectable()
export class LocalStorageService extends StorageService {
  get(key: string) { return localStorage.getItem(key); }
  set(key: string, value: string) { localStorage.setItem(key, value); }
  remove(key: string) { localStorage.removeItem(key); }
}
providers: [
  { provide: StorageService, useClass: LocalStorageService },
]

Multi Providers

Register multiple values under the same token:

export const HTTP_INTERCEPTORS_TOKEN = new InjectionToken<HttpInterceptorFn[]>(
  'http-interceptors'
);

providers: [
  { provide: VALIDATORS, useClass: RequiredValidator, multi: true },
  { provide: VALIDATORS, useClass: MinLengthValidator, multi: true },
  { provide: VALIDATORS, useClass: PatternValidator, multi: true },
]

Injecting VALIDATORS returns an array of all three validators.

Optional and Self/SkipSelf Decorators

Control resolution behavior:

import { inject, Optional, SkipSelf } from '@angular/core';

// Optional — returns null if not found
private analytics = inject(AnalyticsService, { optional: true });

// SkipSelf — skip the current injector, look only at ancestors
private parentState = inject(TreeNodeState, { skipSelf: true });

// Self — only look in the current injector
private localCache = inject(CacheService, { self: true });

Environment Injector for Dynamic Components

Create components dynamically with the correct injector:

import { Component, ViewContainerRef, EnvironmentInjector, inject, createEnvironmentInjector } from '@angular/core';

@Component({ /* ... */ })
export class DynamicHostComponent {
  private vcr = inject(ViewContainerRef);
  private envInjector = inject(EnvironmentInjector);

  loadWidget(component: Type<unknown>) {
    this.vcr.clear();
    this.vcr.createComponent(component, {
      environmentInjector: this.envInjector,
    });
  }
}

Functional Providers with makeEnvironmentProviders

Package a set of providers into a single callable function:

import { makeEnvironmentProviders, Provider } from '@angular/core';

export function provideFeatureFlags(flags: Record<string, boolean>) {
  return makeEnvironmentProviders([
    { provide: FEATURE_FLAGS, useValue: flags },
    FeatureFlagService,
  ]);
}
// app.config.ts
providers: [
  provideFeatureFlags({ darkMode: true, betaSearch: false }),
]

Best Practices

  1. Default to providedIn: 'root' for application-wide singletons. It is tree-shakable and requires no explicit provider registration.

  2. Use component-level providers for state that should be scoped. Form state, editor state, and wizard state are good candidates for component-provided services.

  3. Prefer inject() over constructor parameters. It is more concise, works in field initializers, and enables better refactoring.

  4. Use InjectionToken for configuration and non-class values. Never use raw strings as tokens — they are not type-safe and collide easily.

  5. Design for testability. Every service that depends on browser APIs (localStorage, fetch, window) should be behind an injectable abstraction so tests can substitute fakes.

  6. Avoid deep provider hierarchies. If debugging "which instance am I getting?" becomes difficult, simplify the provider tree.

Common Pitfalls

  • Circular dependencies. Service A injects Service B which injects Service A. Angular throws a cyclic dependency error. Break the cycle by introducing a mediator service or using forwardRef.

  • Missing provider. NullInjectorError: No provider for X means no injector in the chain has registered the dependency. Ensure the service is provided at the correct level.

  • Unintended singletons. Providing a service at root when you need per-component instances means all components share the same state. Move the provider to the component level.

  • Using useClass without realizing it creates a new class. { provide: A, useClass: B } creates a new instance of B. If B is also providedIn: 'root', you now have two instances. Use useExisting if you want to alias to the existing singleton.

  • Forgetting multi: true on multi-providers. Without multi: true, each provider registration replaces the previous one instead of appending.

Install this skill directly: skilldb add angular-skills

Get CLI access →