This article is created by heavily using Claude.ai.
The article banner is a picture of Graphue
Angular's Dependency Injection (DI) system is powerful, but knowing when to use constructor injection versus the newer inject()
function can significantly improve your code quality. With the introduction of runInInjectionContext()
, we now have even more control over dependency resolution.
In this article, we'll explore five key scenarios where inject()
(sometimes with runInInjectionContext
) provides better solutions than traditional constructor injection.
1. Standalone Functions (Route Guards, Interceptors)
Problem: Constructor injection requires class wrappers for simple functions.
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private authService: AuthService
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
return this.authService.isLoggedIn() || this.router.navigate(['/login']);
}
}
Solution: inject()
enables dependency access in functions.
import { inject } from '@angular/core';
import { Router } from '@angular/router';
export function authGuard(): boolean {
const router = inject(Router);
const authService = inject(AuthService);
return authService.isLoggedIn() || router.navigate(['/login']);
}
Key Benefits:
less boilerplate code
No artificial class creation
Better tree-shakability
Now the Angular-recommended approach.
As you may notice it is used with functional route guards.
2. Factory Functions (Creating Customized Services)
As for me - this case is similar to previous one but have different application point - creating configurable service instances in functional way.
import { inject, InjectionToken } from '@angular/core';
// Token for different logger instances
export const USER_LOGGER = new InjectionToken<Logger>('user.logger');
export const SYSTEM_LOGGER = new InjectionToken<Logger>('system.logger');
// A factory that creates specialized logger instances
export function createLogger(category: string, minLevel: 'debug'|'info'|'error') {
const configService = inject(ConfigService);
return {
debug: (msg: string) => {
if (minLevel === 'debug' && configService.isDebugEnabled) {
console.log(`[${category}][DEBUG] ${msg}`);
}
},
info: (msg: string) => {
if (minLevel === 'debug' || minLevel === 'info') {
console.log(`[${category}][INFO] ${msg}`);
}
},
error: (msg: string) => {
console.error(`[${category}][ERROR] ${msg}`);
}
};
}
// Creating provideFunction
export function provideUserLogger(minLevel: 'debug'|'info'|'error') {
return {
provide: USER_LOGGER,
useFactory: () => createLogger('USER', minLevel)
}
}
export function provideSystemLogger(minLevel: 'debug'|'info'|'error') {
return {
provide: SYSTEM_LOGGER,
useFactory: () => createLogger('SYSTEM', minLevel)
}
}
// Register in config file which will be used for Angular pp bootstrapping
export const appConfig: ApplicationConfig = {
providers: [
// Other providers...
ConfigService,
provideUserLogger('info'),
provideSystemLogger('error')
]
};
Well, remembering to provide ConfigService to be able to use SYSTEM_LOGGER and USER_LOGGER providers is not very convenient, so better to modify provideSystemLogger and provideUserLogger to do that for us:
export function provideUserLogger(minLevel: 'debug'|'info'|'error') {
return [
ConfigService,
{
provide: USER_LOGGER,
useFactory: () => createLogger('USER', minLevel),
}]
}
export function provideSystemLogger(minLevel: 'debug'|'info'|'error') {
return [
ConfigService,
{
provide: SYSTEM_LOGGER,
useFactory: () => createLogger('SYSTEM', minLevel),
}]
}
Here is a playground.
3. Lazy Injection (On-Demand Service Loading)
Constructor injection instantiates all services upfront, even if they’re rarely used. inject() allows delayed injection.
It work by using the runInInjectionContext function. This function lets you create an injection context at runtime, allowing inject() to be called outside the component initialization.
import { Component, inject, Injector, runInInjectionContext } from '@angular/core';
import { HeavyDataService } from './services';
@Component({
selector: 'app-heavy',
template: '...'
})
export class HeavyComponent {
// Store the injector itself
private injector = inject(Injector);
// Service is not injected at initialization
private loadHeavyData() {
// Create injection context when needed
return runInInjectionContext(this.injector, () => {
// Now we can safely use inject()
const heavyService = inject(HeavyDataService);
return heavyService.fetchData();
});
}
onUserAction() {
// Heavy service only injected when this is called
const data = this.loadHeavyData();
// Process data...
}
}
Looks good, doesn't it? Until my friend told me that to make HeavyDataService really lazy we need to use import O_o:
private loadHeavyData() {
// Create injection context when needed
return runInInjectionContext(this.injector, () => {
// Now we can safely use inject()
return import('./services').then(({ HeavyDataService }) => {
const heavyService = inject(HeavyDataService);
return heavyService.fetchData();
});
}
4. Multi-Level Inheritance (Avoiding Constructor Hell)
TBH I am not a fan of inheriting component classes in Angular (composition over inheritance). I think it brings more mess than DRY benefits. But if you like it:
@Component({...})
export class BaseComponent {
protected readonly router = inject(Router);
}
@Component({...})
export class MiddleComponent extends BaseComponent {
protected readonly userService = inject(UserService);
}
@Component({...})
export class ChildComponent extends MiddleComponent {
// Has access to both `router` and `userService`
// No complex constructor chaining needed!
}
Benefits:
No super() boilerplate.
Cleaner, more maintainable inheritance.
A child class can override a parent’s injected service by using inject() with the same token, creating its own instance.
5. Dynamic Providers (Runtime Dependency Switching)
Constructor injection requires static dependencies. inject() enables dynamic service selection
This case is very close to example #3 with Lazy service instantiation but brings more conditional logic to the previous example.
import { Component, inject, Injector, runInInjectionContext } from '@angular/core';
import { FeatureFlagService, NewImplementationService, LegacyImplementationService } from './services';
@Component({
selector: 'app-dynamic',
template: '...'
})
export class DynamicComponent {
private injector = inject(Injector);
private featureFlag = inject(FeatureFlagService);
// Don't inject services yet
private service: any;
constructor() {
// Get the right service based on feature flag
this.service = this.getService();
}
private getService() {
return runInInjectionContext(this.injector, () => {
// Now we can conditionally inject
if (this.featureFlag.isNewFeatureEnabled) {
return inject(NewImplementationService);
} else {
return inject(LegacyImplementationService);
}
});
}
doSomething() {
this.service.method();
}
}
6. Type Inference with inject() vs Constructor DI
When using InjectionTokens with constructor DI, you need to manually specify the type. Even more: you can get the type annotation wrong. Angular/TypeScript doesn't check whether the type annotation matches the type of the InjectionToken/class.
// Define a token with a type
const CONFIG = new InjectionToken<AppConfig>('app.config');
@Component({...})
class MyComponent {
constructor(
// Must manually specify the type here
@Inject(CONFIG) private config: AppConfig
) {}
}
Moreover, Constructor parameter decorators are not part of the ECMAScript Decorators standard. They will be unsupported once the experimentalDecorators option is removed from the TypeScript compiler
The inject() function automatically infers the correct type from the token:
typescript// Define a token with a type
const CONFIG = new InjectionToken<AppConfig>('app.config');
@Component({...})
class MyComponent {
// Type is automatically inferred as AppConfig
private config = inject(CONFIG);
ngOnInit() {
// TypeScript knows this is AppConfig
console.log(this.config.apiUrl);
}
}
7. Changes in ES2022 and future deprecation of 'useDefineForClassFields' compiler option in typescript.
This info was introduced by Jeremy Elbourn (team lead of Angular team) in github issue comment:
Additionally, we're adding one new recommendation: Prefer the inject function over constructor parameter injection
We're adding this new recommendation in light of the introduction of class fields in ECMAScript 2022.
Next text is copied from Jeremy Elbourn's github comment:
Here is a basic example:
@Component({ /* ... */ })
export class UserProfile {
private user = this.userData.getCurrent();
constructor(private userData: UserData) { }
}
This example works just fine when TypeScript emits ECMAScript versions less than ES2022. In these versions, the compiled JavaScript looks like this:
// Emitting ES2017
export class UserProfile {
constructor(userData) {
// The field initializer is inlined into the constructor
this.userData = userData;
this.user = this.userData.getCurrent();
}
}
However, in ES2022 with the useDefineForClassFields option, the output looks like this:
// Emitting ES2022
export class UserProfile {
userData;
user = this.userData.getCurrent(); // Error! userData is not yet initialized!
constructor(userData) {
this.userData = userData;
}
}
This output throws an error because the field initializer runs before the constructor and tries to use the injected dependency before it's available. To work around this with constructor injection, you would write your code like this:
@Component({ /* ... */ })
export class UserProfile {
// Field declaration is separated from initialization.
private user: User;
constructor(private userData: UserData) {
this.user = userData.getCurrent();
}
}
Many developers find the separation of field declaration and initialization to be undesirable. Fortunately, the inject function neatly sidesteps this problem:
@Component({ /* ... */ })
export class UserProfile {
private userData = inject(UserData);
private user = this.userData.getCurrent();
}
Appendix: Situations Where inject() Won't Work in Angular
1. Outside Injection Context (more here)
Here are particular cases:
a) In Asynchronous Code
@Component({...})
class MyComponent {
constructor() {
// This will fail
setTimeout(() => {
const service = inject(MyService); // ERROR
}, 1000);
}
}
b) In Event Handlers
@Component({
template: '<button (click)="handleClick()">Click</button>'
})
class MyComponent {
handleClick() {
// ERROR: No injection context in event handler
const service = inject(MyService);
}
}
c) In Subscription Callbacks
@Component({...})
class MyComponent {
constructor() {
const observable = inject(DataService).getData();
observable.subscribe(data => {
// ERROR: No injection context in subscription callback
const logger = inject(LoggerService);
logger.log(data);
});
}
}
d) In Standalone Functions Without runInInjectionContext
// Standalone utility function
export function formatData(data: any) {
// ERROR: No injection context
const formatter = inject(FormatterService);
return formatter.format(data);
}
e) inject(...) won't work in ngOnInit method without runInInjectionContext.
// doesn't work
ngOnInit(): void {
// We need to use runInInjectionContext here
this.serviceB = inject(MyService);
}
// works
ngOnInit(): void {
runInInjectionContext(this.injector, () => {
this.serviceB = inject(MyService);
});
}
Actually, all the cases above are solved by wrapping in runInInjectionContext helper function.
2. In Regular (Non-Angular) Classes
// Regular class, not managed by Angular DI
class RegularClass {
constructor() {
// ERROR: No injection context
const service = inject(SomeService);
}
}
Final Verdict
Use constructor injection for standard component/service DI but consider moving to inject() if you plan to migrate to es2022.
Use inject() for edge cases like functions, factories, lazy loading, inheritance, and dynamic providers
More to read:
- "The inject function is not a service locator" by Matthieu Riegler
- Interesting drawback of using inject function in Angular 16+ (I did not check it on newer versions). Case: Angular 16+ pipe used with the template child component input value. Drawback: if in pipe we inject ChangeDetectorRef with 'inject' function: cdRef will be from ChildComponent; but if we inject ChangeDetectorRef using constructor injection, it will be from the ParentComponent.
Like this article? Follow me on Twitter!
Top comments (4)
Just a doubt about this
d) In Standalone Functions Without runInInjectionContext
// Standalone utility function
export function formatData(data: any) {
// ERROR: No injection context
const formatter = inject(FormatterService);
return formatter.format(data);
}
It will depend on how we use the function won’t it? I mean if we later use that function within an angular component in a the context of the member initialisation it will work right ? No need to use runInInjectionContext. Angular will now how to handle that.
I might be wrong
Thanks in advance
Yeah, sure. If we use in in injectionContext phase - no need to wrap in runInInjectionContext
Hi thanks for the detailed explanation.
Should we use this.userData.getCurrent() instead of userData.getCurrent()? In the example below?
@Component({ /* ... */ })
export class UserProfile {
private userData = inject(UserData);
private user = userData.getCurrent();
}
Thanks
You are right, thank you for noticing that