result;
}
}
**Rationale:** `@Service()` eliminates the object literal configuration required by `@Injectable`. The service is a singleton by default. We use signals for state management, which integrates seamlessly with Angular's reactive rendering.
#### 2. Functional Injection with `inject()`
The `inject()` function is the standard for retrieving dependencies. It must be called within an injection context (e.g., class field, constructor, or factory).
```typescript
import { Component, inject, computed } from '@angular/core';
import { PaymentGateway } from './payment.gateway';
@Component({
selector: 'app-checkout',
standalone: true,
template: `
<div *ngIf="gateway.lastTransaction() as txn">
Status: {{ txn.status }} | ID: {{ txn.transactionId }}
</div>
`
})
export class CheckoutComponent {
// Field injection is the preferred pattern
protected readonly gateway = inject(PaymentGateway);
// Computed values can depend on injected services
protected readonly isPaymentComplete = computed(() =>
this.gateway.lastTransaction()?.status === 'success'
);
handlePayment() {
this.gateway.process(99.99, 'USD');
}
}
Rationale: Field injection via inject() is cleaner than constructor injection. It allows dependencies to be declared alongside other class members and supports type inference without explicit type annotations. Note that inject() cannot be used inside lifecycle hooks like ngOnInit; it must be called during class instantiation.
3. Scoped Instances via Component Providers
Root singletons are not always appropriate. When a component requires isolated state that should not persist across navigation or sibling instances, provide the service at the component level.
import { Component, inject, Service } from '@angular/core';
@Service()
export class CartState {
items: string[] = [];
addItem(item: string) {
this.items.push(item);
}
}
@Component({
selector: 'app-cart-widget',
standalone: true,
providers: [CartState], // Creates a new instance for this component tree
template: `<p>Items: {{ cart.items.length }}</p>`
})
export class CartWidgetComponent {
private readonly cart = inject(CartState);
}
Rationale: Adding CartState to the providers array creates a new injector for this component and its children. This instance shadows any root provider. This is essential for features like wizards, multi-step forms, or isolated dashboards where state must reset per instance.
4. Configuration with InjectionToken
TypeScript interfaces are erased at runtime and cannot be used as DI tokens. For non-class dependencies like configuration objects, strings, or primitives, use InjectionToken.
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiEndpoint: string;
maxRetries: number;
}
// The string identifier is used for debugging purposes
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
Provide the token during application bootstrap:
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { APP_CONFIG } from './app.config';
bootstrapApplication(AppComponent, {
providers: [
{
provide: APP_CONFIG,
useValue: {
apiEndpoint: 'https://api.production.example.com',
maxRetries: 3
}
}
]
});
Inject the configuration where needed:
import { inject } from '@angular/core';
import { APP_CONFIG } from './app.config';
export class ApiService {
private readonly config = inject(APP_CONFIG);
getBaseUrl() {
return this.config.apiEndpoint;
}
}
Rationale: InjectionToken provides type safety at compile time while generating a unique runtime object that the DI container can resolve. This pattern decouples configuration from implementation, enabling environment-specific overrides and easier testing.
5. Dynamic Providers with useFactory
When a dependency requires runtime computation or depends on other injected values, use useFactory.
import { inject, Provider } from '@angular/core';
import { APP_CONFIG } from './app.config';
import { LoggerService } from './logger.service';
export const LOGGER_PROVIDER: Provider = {
provide: LoggerService,
useFactory: () => {
// inject() can be used inside the factory function
const config = inject(APP_CONFIG);
const level = config.maxRetries > 2 ? 'verbose' : 'standard';
return new LoggerService(level);
}
};
Rationale: useFactory allows for conditional logic and dependency composition. The factory function runs within an injection context, permitting the use of inject() to retrieve other providers. This is superior to useClass when the implementation requires constructor arguments derived from configuration.
Pitfall Guide
1. The Interface Trap
Explanation: Attempting to use a TypeScript interface as a DI token results in a runtime error. Interfaces exist only at compile time and are removed during transpilation.
Fix: Always wrap interfaces in an InjectionToken.
// ❌ BAD: Fails at runtime
// inject(MyInterface)
// ✅ GOOD
export const MY_INTERFACE_TOKEN = new InjectionToken<MyInterface>('my.interface');
inject(MY_INTERFACE_TOKEN);
2. Constructor Injection with @Service()
Explanation: In Angular 22+, services decorated with @Service() do not support constructor injection. Attempting to inject dependencies via the constructor will fail because @Service() does not emit the necessary metadata.
Fix: Use inject() for all dependencies within @Service() classes.
// ❌ BAD: Constructor injection fails with @Service()
@Service()
export class MyService {
constructor(private http: HttpClient) {} // Error
}
// ✅ GOOD
@Service()
export class MyService {
private http = inject(HttpClient);
}
3. Singleton Leakage
Explanation: Providing a service in a component when a root singleton is expected (or vice versa) leads to state inconsistency. If a service is provided in a lazy-loaded module without providedIn: 'root', Angular creates a separate instance for that module, breaking shared state.
Fix: Use providedIn: 'root' or @Service() for shared state. Use component providers only for isolated state. Verify provider hierarchy using Angular DevTools.
4. Circular Dependencies
Explanation: Service A depends on Service B, and Service B depends on Service A. This causes a stack overflow or injection error during instantiation.
Fix: Refactor to break the cycle. Common solutions include:
- Extract shared logic into a third service.
- Use
forwardRef() if the cycle is unavoidable (though this indicates a design smell).
- Inject one dependency lazily using
inject() inside a method rather than at the field level.
5. Missing Optional Flag
Explanation: Injecting a dependency that may not be provided in certain contexts (e.g., a service provided only in specific modules) throws an error if the provider is absent.
Fix: Use the optional flag to return null instead of throwing.
// ✅ GOOD: Returns null if AnalyticsService is not provided
private analytics = inject(AnalyticsService, { optional: true });
trackEvent() {
this.analytics?.log('event');
}
6. Injection Context Violation
Explanation: Calling inject() outside of an injection context (e.g., inside a regular function, a setTimeout callback, or a lifecycle hook) throws an error.
Fix: Ensure inject() is called during class instantiation (fields, constructor) or within a provider factory. For lifecycle hooks, store the injected value in a field during initialization.
7. Factory Dependency Omission
Explanation: When using useFactory, failing to inject dependencies correctly can lead to undefined values. While inject() works inside factories, mixing deps array with inject() can cause confusion.
Fix: Prefer inject() inside the factory function for modern code. If using deps, ensure the array order matches the function parameters exactly.
// ✅ GOOD: Using inject() inside factory
useFactory: () => {
const config = inject(APP_CONFIG);
return new Service(config);
}
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Global State Service | @Service() with inject() | Tree-shakable, singleton, minimal boilerplate. | Low |
| Component-Local State | Component providers array | Isolates state per instance; prevents cross-contamination. | Medium (memory per instance) |
| Static Configuration | InjectionToken + useValue | Type-safe, decouples config from logic, easy to mock. | Low |
| Conditional Logic | useFactory with inject() | Allows runtime computation based on other providers. | Medium (factory execution overhead) |
| Mocking in Tests | useClass or useValue | Swaps implementation without changing consumer code. | Low |
| Legacy Module Service | @Injectable() + Module providers | Required for non-standalone legacy modules. | High (disables tree-shaking) |
Configuration Template
This template demonstrates a production-ready configuration setup using InjectionToken and useFactory to handle environment-specific settings with fallbacks.
import { InjectionToken, Provider, inject } from '@angular/core';
export interface FeatureFlags {
enableDarkMode: boolean;
maxUploadSize: number;
}
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('feature.flags');
// Factory with fallback logic
export const FEATURE_FLAGS_PROVIDER: Provider = {
provide: FEATURE_FLAGS,
useFactory: () => {
// Example: Inject a runtime config service if available
// const runtimeConfig = inject(RuntimeConfigService, { optional: true });
return {
enableDarkMode: true,
maxUploadSize: 10 * 1024 * 1024 // 10MB
};
}
};
// Usage in service
export class ThemeService {
private flags = inject(FEATURE_FLAGS);
isDarkModeEnabled(): boolean {
return this.flags.enableDarkMode;
}
}
Quick Start Guide
-
Create Service: Generate a new service file and decorate the class with @Service().
ng generate service services/data-fetcher
Update the decorator to @Service().
-
Inject Dependency: In your component or another service, use inject() to retrieve the service.
private dataFetcher = inject(DataFetcherService);
-
Provide Configuration: If the service requires configuration, define an InjectionToken and provide it in the bootstrap providers or a parent component.
bootstrapApplication(AppComponent, {
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
});
-
Verify Scope: Run the application and check the injector hierarchy. Ensure root services are singletons and component services are scoped correctly. Use Angular DevTools to inspect the injector tree.
-
Build and Analyze: Run ng build --configuration production and analyze the bundle. Confirm that unused services are tree-shaken and the application size is optimized.