Here’s a concise, team‑friendly guideline for when to use constructor injection vs inject()
in Angular (v16+ through v20), aligned with standalone components and typed DI.
inject()
where constructors are awkward or unavailable: field initializers, base classes to avoid super(...)
churn, provider/InjectionToken
factories, router guards/resolvers, and when you need runInInjectionContext(...)
.Injector
in a static). inject()
is contextual DI, not a locator.inject()
API & injection context; Fowler’s DI definition 1234inject()
are still dependency injection in Fowler’s sense—dependencies are supplied by the container, not constructed or globally looked up by the class. 4inject()
is contextual: it only works in an injection context (e.g., class creation by Angular, field initializers of DI‑created classes, provider/InjectionToken
factories, router guards; otherwise NG0203
). 21inject()
use can hide them and hinder test ergonomics. Use it judiciously. 31) Angular‑decorated classes with stable deps
Components, directives, pipes, and services with a predictable set of dependencies.
@Component({standalone: true, /* … */})
export class ProfileComponent {
constructor(private readonly user: UserService) {}
}
Why: explicit API surface, straightforward unit tests (new Class(mock)
), and familiar to most devs. 12
2) You want maximum dependency visibility
Code reviews should see what the class needs at a glance (esp. in app-critical or shared libs).
3) You instantiate classes outside Angular
If something may be constructed directly in tests/tools, constructor params are simpler.
inject()
when…1) Field initializer ergonomics
You want to avoid boilerplate constructors:
@Injectable({providedIn: 'root'})
export class FeatureService {
private readonly http = inject(HttpClient);
}
Note: still runs in DI context because Angular creates the class. 12
2) Inheritance without constructor churn
Base classes can inject()
their own deps, so subclasses don’t have to call super(...)
. 3
abstract class BaseVm {
protected readonly store = inject(Store);
}
export class OrdersVm extends BaseVm {
// no super(store) noise
}
3) Functional/Factory APIs
Router guards/resolvers, provider factories, and InjectionToken
factories:
export const canActivateTeam: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.isAllowed();
};
export const API_URL = new InjectionToken<string>('API_URL', {
factory: () => inject(ConfigService).apiUrl
});
These are designed to run in an injection context. 2
4) You need runInInjectionContext
To execute code that requires DI while you hold an EnvironmentInjector
:
const env = inject(EnvironmentInjector);
runInInjectionContext(env, () => {
const logger = inject(Logger);
logger.info('within context');
});
This is the official way to establish an injection context manually. 2
5) Optional/self/host/skipSelf flags inline
inject(TOKEN, {optional: true, self: true})
replaces multiple parameter decorators. 1
Global service locator
Do not store a static Injector
and call get()
from anywhere. That hides dependencies and breaks DI clarity. inject()
already enforces context; keep it that way. 42
Scattered inject()
calls inside methods
Keep all inject()
calls at top-level fields in the class. Don’t dynamically inject inside loops or deep in methods; it hides deps and complicates tests. 3
Calling inject()
outside context
If you’re not in a DI context, use runInInjectionContext(envInjector, () => ...)
. 2
Constructor injection classes
Prefer plain unit tests: const svc = new Class(mockDep as any)
; no TestBed
needed.
Classes using inject()
Use Angular testing utilities to provide the injection context:
TestBed.configureTestingModule({ providers: [Logger] });
const logger = TestBed.inject(Logger); // or create component/fixture
For factory/functional code, wrap with runInInjectionContext(TestBed, () => ...)
(or use a fixture) to ensure a context exists. 21
inject(...)
fields at the top of the class, after the decorator.private readonly http = inject(HttpClient)
(lowerCamelCase, readonly
).inject(Token, { optional: true })
and handle null
paths explicitly. 1inject()
.Situation | Use | Rationale |
---|---|---|
Component/directive/pipe/service with stable deps | Constructor | Explicit deps, easy unit tests, standard practice. 12 |
Base class (avoid super(...) ) |
inject() |
Cleaner inheritance. 3 |
Router guard/resolver, provider/InjectionToken factory |
inject() |
These APIs run in DI context by design. 2 |
Code needs DI but you only have an EnvironmentInjector |
runInInjectionContext + inject() |
Establishes context on demand. 2 |
Utils or classes constructed manually (outside Angular) | Constructor | No DI context; keeps tests trivial. 4 |
Constructor first, simple service
@Injectable({ providedIn: 'root' })
export class OrdersService {
constructor(private readonly http: HttpClient) {}
}
Base class using inject()
abstract class BaseEffects {
protected readonly actions$ = inject(Actions);
protected readonly store = inject(Store);
}
@Injectable({ providedIn: 'root' })
export class OrdersEffects extends BaseEffects {
// no constructor churn
}
Router guard with inject()
export const canActivateOrders: CanActivateFn = () => {
const auth = inject(AuthService);
return auth.canAccess('orders');
};
Factory with runInInjectionContext
@Injectable({ providedIn: 'root' })
export class ReportsService {
private readonly env = inject(EnvironmentInjector);
runTask() {
runInInjectionContext(this.env, () => {
inject(Logger).info('task running');
});
}
}
inject()
fields only).inject()
calls inside methods/loops.Injector
access or “service locator” helpers.TestBed
when constructor DI would suffice.runInInjectionContext
used only when truly outside an injection context.If you want, I can turn this into an ESLint rule set (or Angular ESLint config) that flags disallowed inject()
placements and static Injector
usage.
Angular API – inject()
(usage, options, contexts). https://angular.dev/api/core/inject ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
Angular Guide – Injection context (NG0203
, router guards, runInInjectionContext
, field initializers). https://angular.dev/guide/di/dependency-injection-context ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
“Angular’s inject()
a couple of years later…” (pros/cons, inheritance ergonomics, visibility concerns). https://dev.to/this-is-angular/angulars-inject-function-a-couple-of-years-later-5gl1 ↩ ↩2 ↩3 ↩4 ↩5
Martin Fowler – Inversion of Control Containers and the Dependency Injection pattern (DI definition, forms, vs Service Locator). https://martinfowler.com/articles/injection.html ↩ ↩2 ↩3 ↩4