Tutorials Logic, IN info@tutorialslogic.com

Dependency Injection in Angular inject

Injection Workflow

Angular dependency injection resolves collaborators from hierarchical injectors. This lesson covers provider records, inject and constructor injection, InjectionToken, and the lifetime changes caused by root, route, and component scopes.

Dependency Injection Model

Dependency injection lets a class request collaborators instead of constructing them. Angular resolves each request from an active injector, which makes ownership, replacement in tests, and lifetime explicit.

Code requests a token; the injector finds a provider recipe, creates or returns the value, and caches it at that provider boundary. A class can be its own token, while InjectionToken represents configuration, functions, interfaces, and other values without a runtime class.

Injector Resolution

Angular starts with the nearest applicable element injector and then searches ancestor element and environment injectors. A nearer provider shadows the same token above it, which enables feature overrides but can accidentally split state that developers expected to share.

A route provider lives with that route environment. A component provider belongs to one component subtree, and each component instance receives a separate scoped service.

  • EnvironmentInjector: owns application and route-level providers; providedIn: "root" normally creates one application-wide instance.
  • ElementInjector: exists at each DOM element and gains providers through component or directive metadata.
  • ModuleInjector: supplies the corresponding hierarchy in applications that bootstrap through NgModules.

The inject Function

The inject function retrieves a token with accurate inferred types and works well in field initializers and provider factories. Constructor injection remains supported; choose one style consistently within a class.

inject works only in an injection context, including field initialization, construction, provider factories, and functions deliberately run in that context. Calling it later from an arbitrary event handler fails because no active injector is available.

Options support optional lookup and ancestor controls such as self, skipSelf, and host. Treat an optional result as nullable instead of asserting that a provider must exist.

Field Injection with inject

Field Injection with inject
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `
    <h2>Dashboard</h2>
    <p>Count: {{ counter.count() }}</p>
    <button (click)="navigate()">Go Home</button>
  `
})
export class DashboardComponent {
  // Modern inject() style - no constructor needed
  private http    = inject(HttpClient);
  private router  = inject(Router);
  counter         = inject(CounterService);

  navigate() { this.router.navigate(['/home']); }
}

Equivalent Constructor Injection

Equivalent Constructor Injection
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  template: `<h2>Dashboard</h2>`
})
export class DashboardComponent {
  // Traditional constructor injection
  constructor(
    private http: HttpClient,
    private router: Router,
    public counter: CounterService
  ) { }
}

Provider Recipes and Injection Tokens

A provider maps a token to a creation recipe. Use a class provider for a concrete implementation, a value provider for immutable configuration, a factory provider when construction depends on other tokens, and an existing provider when two tokens must resolve to the same instance.

Use InjectionToken for configuration values, interfaces, callbacks, and collections that do not exist as runtime classes. Give every token a descriptive name and a precise generic type so incorrect providers fail during compilation.

  • useClass substitutes one implementation for a token; the replacement gets its own dependency graph.
  • useValue supplies an already-created value. Treat configuration objects as readonly.
  • useFactory computes a value and can call inject inside the factory.
  • useExisting aliases another token and preserves identity; useClass would create a separate instance.
  • multi: true collects every provider for the token into one array, which is useful for hooks and extension points.

Typed Configuration and Factory Providers

Typed Configuration and Factory Providers
import { InjectionToken, inject } from '@angular/core';

export interface ApiConfig {
  readonly baseUrl: string;
  readonly timeoutMs: number;
}

export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
export const API_URL = new InjectionToken<string>('API_URL');

export const apiProviders = [
  {
    provide: API_CONFIG,
    useValue: { baseUrl: '/api', timeoutMs: 5000 } satisfies ApiConfig
  },
  {
    provide: API_URL,
    useFactory: () => inject(API_CONFIG).baseUrl
  }
];

Alias an Implementation without Duplicating It

Alias an Implementation without Duplicating It
abstract class AuditSink {
  abstract record(event: string): void;
}

@Injectable()
class BrowserAuditSink implements AuditSink {
  record(event: string): void {
    console.info(event);
  }
}

const auditProviders = [
  BrowserAuditSink,
  { provide: AuditSink, useExisting: BrowserAuditSink }
];

// Both tokens resolve to the same BrowserAuditSink object.

Provider Scope

Use providedIn: "root" for a tree-shakable service that should normally be shared. Use a component provider for disposable subtree state such as one editor session, and a route provider for state shared inside one lazy feature.

Do not provide a stateful service separately in siblings that must share one instance. Move the provider to their nearest common owner and verify that leaving the feature destroys the intended state.

Component-scoped Provider

Component-scoped Provider
import { Component, inject } from '@angular/core';
import { CounterService } from './counter.service';

@Component({
  selector: 'app-root',
  standalone: true,
  // Each instance of this component gets its OWN CounterService
  providers: [CounterService],
  template: `
    <p>Count: {{ counter.count() }}</p>
    <button (click)="counter.increment()">+</button>
  `
})
export class AppComponent {
  counter = inject(CounterService);
}

Injection Context and Test Overrides

Code that calls inject must run while Angular has an active injector. Field initializers, constructors, provider factories, router guards, and functions passed to runInInjectionContext are valid locations. A timer callback or ordinary utility function is not automatically an injection context.

Tests can replace a token without changing production code. Configure the real dependency only when the test needs its behavior; otherwise provide a focused fake and assert the class contract. TestBed.runInInjectionContext is useful for testing injectable functions such as guards.

  • Capture dependencies during construction instead of calling inject later inside event callbacks.
  • Use TestBed.overrideProvider or a test provider before the first injection occurs.
  • Reset or recreate the testing injector when a provider lifetime is part of the assertion.

Override a Token in a Test

Override a Token in a Test
TestBed.configureTestingModule({
  providers: [
    PriceService,
    { provide: API_CONFIG, useValue: { baseUrl: '/test-api', timeoutMs: 10 } }
  ]
});

const service = TestBed.inject(PriceService);
expect(service.endpoint).toBe('/test-api/prices');
Confirm the page outcome

Injector Review

5 checks
  • I can distinguish a token, a provider recipe, an injector, and the value that is returned.
  • I can choose useClass, useValue, useFactory, useExisting, or InjectionToken for the dependency contract.
  • I know where inject is legal and how optional, self, skipSelf, and host change lookup.
  • I can predict whether root, route, or component providers share or isolate an instance.
  • I can diagnose duplicate state by searching for a nearer provider that shadows the intended one.

Injector Review Questions

A nearer provider is probably shadowing the root provider.

inject() needs an active Angular injection context.

Interfaces and configuration values have no runtime class for Angular to use as a provider key. An InjectionToken gives them one.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.