Dependency Injection
|
This section documents modern, standalone Angular — signals, the built-in This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted. This section’s bibliography lists the reference material consulted while preparing these pages. |
Dependency injection (DI) is how an Angular class asks for the objects it needs instead of constructing them. A tree of injectors resolves each request, so services are shared, swappable, and easy to fake in tests.
Services and providedIn: 'root'
A service is a plain class marked with @Injectable. Setting providedIn: 'root' registers it with the
application’s root injector, making it a singleton that any component or other service can inject, and letting
the bundler tree-shake it away when nothing uses it.
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class BookService {
private readonly http = inject(HttpClient);
list() {
return this.http.get<Book[]>('/api/books');
}
}
Consume it by injecting the class token:
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-book-list',
template: `@for (book of books(); track book.id) { <li>{{ book.title }}</li> }`,
})
export class BookList {
private readonly service = inject(BookService);
readonly books = toSignal(this.service.list(), { initialValue: [] as Book[] });
}
@Injectable(\{ providedIn: 'root' }) is the default the CLI generates with ng generate service book. See
Dependency injection and
Creating and using services.
inject() versus constructor injection
Both forms retrieve the same instance from the same injector. inject() is a function call in a field
initializer; constructor injection uses a typed parameter with an implicit @Inject for the type.
import { Injectable, inject } from '@angular/core';
// preferred: inject() in a field
@Injectable({ providedIn: 'root' })
export class ReportService {
private readonly http = inject(HttpClient);
private readonly books = inject(BookService);
}
// equivalent: constructor parameters
@Injectable({ providedIn: 'root' })
export class LegacyReportService {
constructor(
private readonly http: HttpClient,
private readonly books: BookService,
) {}
}
inject() composes better: it works inside base classes without threading constructor parameters through
subclasses, it needs no constructor at all, and it gives functions (functional guards, interceptors,
resolvers) access to DI. It only works within an injection context.
The injection context
inject() may be called only while Angular is executing constructor-time code: class field initializers and
constructors of injectables, factory functions passed to useFactory or InjectionToken, and the callbacks
of runInInjectionContext. Calling it later — for example inside an event handler or a setTimeout — throws
NG0203.
To reach DI outside that window, capture an Injector (or an EnvironmentInjector) while you still have a
context and re-enter it with runInInjectionContext:
import { Component, EnvironmentInjector, inject, runInInjectionContext } from '@angular/core';
@Component({ selector: 'app-widget', template: '' })
export class Widget {
private readonly injector = inject(EnvironmentInjector);
onClick() {
runInInjectionContext(this.injector, () => {
const books = inject(BookService); // now valid
books.list();
});
}
}
Provider recipes
A provider tells an injector how to produce the value for a token. providedIn uses the class itself as
both token and recipe; an explicit providers entry can decouple the two.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
pageSize: number;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');
export const providers = [
// useClass: satisfy a token with a (possibly different) class
{ provide: BookService, useClass: FakeBookService },
// useValue: a ready-made constant (config, tokens, test doubles)
{ provide: APP_CONFIG, useValue: { apiUrl: '/api', pageSize: 20 } },
// useFactory: compute the value, optionally from other dependencies
{
provide: 'BOOKS_ENDPOINT',
useFactory: (cfg: AppConfig) => `${cfg.apiUrl}/books`,
deps: [APP_CONFIG],
},
// useExisting: an alias -- return the instance already registered for another token
{ provide: LoggerApi, useExisting: ConsoleLogger },
// multi: contribute one entry to an array token instead of replacing it
{ provide: VALIDATORS, useClass: LengthValidator, multi: true },
{ provide: VALIDATORS, useClass: PatternValidator, multi: true },
];
Inject a non-class token with inject(TOKEN):
const config = inject(APP_CONFIG); // typed as AppConfig
const validators = inject(VALIDATORS); // typed as Validator[] because of multi
InjectionToken gives a type-safe key for values that have no class to name them (configuration objects,
strings, feature flags). Provide a factory in its options to make it tree-shakable and root-scoped by
default. See Configuring dependency providers.
Hierarchical injectors
Injectors form a tree with two parallel branches. Angular resolves a token by walking up from the injector
where the request originates until it finds a provider, then throws NG0201 if it reaches the root without a
match.
-
Environment injectors — the root injector created by
bootstrapApplication, plus a child for every lazy route that declaresproviders. Services withprovidedIn: 'root'and everything inApplicationConfiglive here. -
Element injectors — one per component/directive host element, configured by the component’s
providersandviewProvidersmetadata. A child element injector delegates to its parent element injector, and the outermost element injector delegates to the environment injector.
import { Component } from '@angular/core';
@Component({
selector: 'app-cart',
providers: [CartService], // a fresh CartService for this component and its whole subtree
viewProviders: [PricingService], // visible to this component's view, but NOT to projected <ng-content>
template: `<app-cart-line />`,
})
export class Cart {}
Component-level providers create a new instance scoped to that component instance and its descendants — useful for per-feature or per-dialog state. viewProviders is the same but hides the provider from content
projected into the component.
Resolution modifiers
Parameter decorators change how far the lookup walks:
import { Component, Host, Optional, Self, SkipSelf, inject } from '@angular/core';
@Component({ selector: 'app-cart-line', template: '' })
export class CartLine {
// inject() equivalents take an options object
private readonly own = inject(CartService, { self: true }); // only this element injector
private readonly parent = inject(CartService, { skipSelf: true }); // start at the parent
private readonly maybe = inject(PricingService, { optional: true }); // null instead of throwing
private readonly host = inject(CartService, { host: true }); // stop at the host component
// decorator form (constructor injection)
constructor(@Optional() @Self() svc: CartService | null) {}
}
@Self / \{ self: true } restricts the search to the current injector; @SkipSelf starts one level up;
@Host stops at the host component’s element injector; @Optional / \{ optional: true } yields null
rather than an error when nothing is found.
Lazy-loaded routes get their own environment injector, so providers on a lazy route (or via
providers in a route config) are singletons for that lazily loaded area only — a practical way to scope a
service to a feature without making it global. See
Hierarchical injectors.