Angular Dependency Injection Deep Dive: Complete Guide from Basics to Advanced
Master Angular's Dependency Injection system from core concepts to advanced patterns. Learn services, providers, hierarchical injection, and resolution modifiers with practical examples.
Introduction
Have you ever wondered how Angular components magically get access to services without explicitly creating them? That's the power of Dependency Injection (DI)βone of Angular's most important features that helps you write clean, maintainable, and testable code.
π Quick Reference
Reading Time: ~25 minutes
Skill Level: Beginner β Advanced
Prerequisites: Basic TypeScript, Angular components
Navigate by Section (with estimated time):
- Basics (5 min) - What is DI and why use it?
- Services (5 min) - Creating and using services
- inject() Function (3 min) - Modern dependency injection
- Practical Usage (5 min) - Real examples and patterns
- Injection Context (8 min) - Where inject() works and why
- Providers (6 min) - Manual configuration options
- Advanced Topics (8 min) - Hierarchical DI, modifiers, patterns
π‘ New to Angular DI? Start from the top and work your way down.
π― Debugging an error? Jump to Injection Context or Troubleshooting.
What You'll Learn
In this comprehensive guide, we'll explore Angular's Dependency Injection system from the ground up. Whether you're a beginner learning the basics or an experienced developer looking to master advanced patterns, this guide has you covered.
Topics Covered:
- β What Dependency Injection is and why it matters
- β How to create and use services effectively
- β Understanding providers and different provider types
- β Mastering hierarchical injection and injector trees
- β Using resolution modifiers to control dependency lookup
- β
Injection contexts and when you can use
inject() - β Advanced patterns and real-world use cases
- β Common pitfalls and how to avoid them
What is Dependency Injection?
Dependency Injection (DI) is a design pattern that helps organize and share code across your application. It allows you to "inject" features into different parts of your app without manually creating them.
Why Use Dependency Injection?
As applications grow larger, you'll need to reuse and share functionality across different components. DI solves several common challenges:
β Better Code Maintainability β Clean separation of concerns makes refactoring easier and reduces code duplication
β Improved Scalability β Modular functionality can be reused across multiple contexts, making it easier to scale your app
β Easier Testing β DI makes unit testing simple by allowing you to easily use mock services or test doubles instead of real implementations
Understanding Dependencies
A dependency is any object, value, function, or service that a class needs to work but doesn't create itself. In other words, it's something your code relies on to function properly.
There are two key concepts in any DI system:
- Providing β Making values available for injection
- Injecting β Requesting those values as dependencies
Common types of injected dependencies include:
- Configuration values β Environment-specific constants, API URLs, feature flags
- Factories β Functions that create objects or values based on runtime conditions
- Services β Classes that provide common functionality, business logic, or state
Understanding Services
An Angular service is a TypeScript class decorated with @Injectable(), which makes an instance of the class available to be injected as a dependency. Services are the most common way of sharing data and functionality across your application.
Common Types of Services
Services typically handle these responsibilities:
πΉ Data Clients β Handle HTTP requests to retrieve and send data to servers
πΉ State Management β Manage application state shared across multiple components
πΉ Authentication & Authorization β Handle user login, token storage, and access control
πΉ Logging & Error Handling β Provide consistent logging and error reporting
πΉ Event Handling β Manage events or notifications not tied to a specific component
πΉ Utility Functions β Offer reusable functions for data formatting, validation, or calculations
Creating a Service
You can create a service using the Angular CLI:
ng generate service my-service
Or manually by adding the @Injectable() decorator to a TypeScript class:
// π src/app/analytics-logger.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AnalyticsLogger {
trackEvent(category: string, value: string) {
console.log('Analytics event logged:', {
category,
value,
timestamp: new Date().toISOString(),
});
}
}
Note: The providedIn: 'root' option makes this service available throughout your entire application as a singleton (single shared instance). This is the recommended approach for most services.
β οΈ warning[!WARNING] Common Mistake: Many developers forget the
@Injectable()decorator and wonder why their service doesn't work. Always add it, even if you're not injecting dependencies into the service itself! Angular needs this decorator to make the service injectable.
When to Use Which providedIn?
| Option | When to Use | Scope | Example Use Case |
|---|---|---|---|
'root' | β 99% of cases | App-wide singleton | UserService, HttpClient, Logger |
Component providers | Need fresh instance per component | Component tree only | FormStateService, DialogData |
'platform' | β οΈ Rare - multi-app scenarios | Across multiple Angular apps | Shared logging service |
'any' | β οΈ Rare - lazy-loaded modules | Per module instance | Feature-specific cache |
Rule of Thumb: Start with 'root', only change if you have a specific reason!
Injecting Dependencies with inject()
β±οΈ ~3 minutes
Angular provides the inject() function to request dependencies. This is the modern, recommended way to inject services into your components and other services.
Basic Usage Example
Here's an example of a navigation bar component that injects both a custom AnalyticsLogger service and Angular's built-in Router service:
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { AnalyticsLogger } from './analytics-logger.service';
@Component({
selector: 'app-navbar',
template: `
<a href="#" (click)="navigateToDetail($event)">Detail Page</a>
`,
})
export class NavbarComponent {
private router = inject(Router);
private analytics = inject(AnalyticsLogger);
navigateToDetail(event: Event) {
event.preventDefault();
this.analytics.trackEvent('navigation', '/details');
this.router.navigate(['/details']);
}
}
Where Can You Use inject()?
The inject() function works when you're in an injection context. You can use it in:
β
Class constructors β During instantiation of services or components
β
Field initializers β When initializing class properties
β
Factory functions β In useFactory providers
β
InjectionToken factories β When creating custom injection tokens
Example of field initialization:
export class MyComponent {
// These are automatically called in the injection context
private service = inject(MyService);
private router = inject(Router);
}
Creating and Using Services
β±οΈ ~5 minutes
Let's dive deeper into how services work in Angular.
How Services Become Available
When you use @Injectable({ providedIn: 'root' }), Angular:
- β Creates a single instance (singleton) for your entire application
- β Makes it available everywhere without additional configuration
- β Enables tree-shaking so the service is only included in your bundle if it's actually used
This automatic provision is perfect for most use cases!
Example: A Data Store Service
// π src/app/basic-data-store.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class BasicDataStore {
private data: string[] = [];
addData(item: string): void {
this.data.push(item);
}
getData(): string[] {
return [...this.data]; // Return a copy to prevent mutations
}
}
Injecting a Service
Once you've created a service with providedIn: 'root', you can inject it anywhere using the inject() function:
import { Component, inject } from '@angular/core';
import { BasicDataStore } from './basic-data-store.service';
@Component({
selector: 'app-data-display',
template: `
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
`
})
export class DataDisplayComponent {
private dataStore = inject(BasicDataStore);
items = this.dataStore.getData();
}
Injecting Services into Other Services
Services can depend on other services too! This is common for building layered architectures:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AnalyticsLogger } from './analytics-logger.service';
@Injectable({
providedIn: 'root'
})
export class UserService {
private http = inject(HttpClient);
private analytics = inject(AnalyticsLogger);
getUser(id: string) {
this.analytics.trackEvent('api', 'fetch-user');
return this.http.get(`/api/users/${id}`);
}
}
What is an Injection Context?
β±οΈ ~8 minutes
β οΈ Have You Seen This Error?
ERROR: NG0203 - inject() must be called from an injection context
If yes, you're in the right place! This section will make this error disappear forever.
The Problem
Many developers write code like this and get confused:
export class MyComponent {
onClick() {
const router = inject(Router); // β ERROR!
router.navigate(['/home']);
}
}
Why does this break? Let's understand injection context...
The Simple Explanation
Think of an injection context as a "safe zone" where Angular knows how to find and deliver dependencies. It's like being inside a restaurant (injection context) where you can place orders (inject dependencies) versus being outside on the street where the waiters can't serve you (no injection context).
When you call inject(), Angular needs to know which injector to use and where to look for the dependency. The injection context provides this information automatically.
Why Does Injection Context Matter?
Angular's inject() function needs context to work properly because it needs to answer these questions:
- π Which injector should I use? (Component's? Root's? Route's?)
- π Where am I in the component tree? (To search up the hierarchy)
- βοΈ What's the current execution context? (Construction phase? Runtime?)
Without this context, Angular simply doesn't have enough information to resolve dependencies correctly.
When Are You in an Injection Context?
You're automatically in an injection context during these specific phases:
β Safe Places (You ARE in an Injection Context):
1. Class Constructors
@Component({
selector: 'app-demo'
})
export class DemoComponent {
constructor() {
// β
Safe: We're in the constructor
const service = inject(MyService);
}
}
2. Field Initializers
export class DemoComponent {
// β
Safe: Field initializers run during construction
private service = inject(MyService);
private router = inject(Router);
}
3. Provider Factory Functions
const myProvider = {
provide: MyService,
useFactory: () => {
// β
Safe: Factory functions run in injection context
const dependency = inject(SomeDependency);
return new MyService(dependency);
}
};
4. InjectionToken Factories
export const MY_TOKEN = new InjectionToken<string>('my.token', {
providedIn: 'root',
factory: () => {
// β
Safe: Token factory runs in injection context
const config = inject(APP_CONFIG);
return config.apiUrl;
}
});
β Unsafe Places (You are NOT in an Injection Context):
1. Lifecycle Hooks
export class DemoComponent {
ngOnInit() {
// β ERROR: Not in injection context!
const service = inject(MyService);
}
ngOnDestroy() {
// β ERROR: Not in injection context!
const service = inject(MyService);
}
}
2. Event Handlers
export class DemoComponent {
onClick() {
// β ERROR: Not in injection context!
const service = inject(MyService);
}
}
3. Async Callbacks
export class DemoComponent {
loadData() {
setTimeout(() => {
// β ERROR: Not in injection context!
const service = inject(MyService);
}, 1000);
fetch('/api/data').then(() => {
// β ERROR: Not in injection context!
const service = inject(MyService);
});
}
}
The Fix: Inject Early, Use Later
The solution is simple: always inject dependencies during initialization, then use them later:
π Real-World Fix: Before & After
β Before (Doesn't Work)
export class MyComponent {
router: Router; // undefined initially
ngOnInit() {
// β ERROR: Not in injection context!
this.router = inject(Router);
}
onClick() {
this.router.navigate(['/home']);
}
}
β After (Works Perfectly)
export class MyComponent {
// β
Field initializer = injection context
// This runs during component construction
router = inject(Router);
ngOnInit() {
// β
Use the already-injected service
console.log('Router is ready!');
}
onClick() {
// β
Use it anywhere in the component
this.router.navigate(['/home']);
}
}
What Changed?
- Moved
inject()fromngOnInit()(runtime) to field initializer (construction phase) - Field initializers run during component initialization = injection context β
- Lifecycle hooks run after initialization = NOT injection context β
Key Insight: inject() only works during initialization. Use the injected value afterwards!
export class DemoComponent {
// β
Inject in field initializer (injection context)
private service = inject(MyService);
private router = inject(Router);
ngOnInit() {
// β
Use the already-injected service
this.service.doSomething();
}
onClick() {
// β
Use the already-injected service
this.router.navigate(['/home']);
}
async loadData() {
const data = await fetch('/api/data');
// β
Use the already-injected service
this.service.processData(data);
}
}
Advanced: runInInjectionContext()
Sometimes you legitimately need to run code in an injection context later (though this is rare). Angular provides runInInjectionContext() for these cases:
import { runInInjectionContext, inject, Injector } from '@angular/core';
export class DemoComponent {
private injector = inject(Injector);
doSomethingLater() {
// Create an injection context manually
runInInjectionContext(this.injector, () => {
// β
Now we're in an injection context!
const service = inject(MyService);
service.doWork();
});
}
}
When to use this:
- Creating dynamic components or directives at runtime
- Building advanced libraries or frameworks
- Working with third-party code that needs dependency injection
When NOT to use this:
- Regular component development (just inject in field initializers)
- Lifecycle hooks (inject the dependency earlier)
- Event handlers (inject the dependency earlier)
Real-World Example
Here's how a typical component uses injection context correctly:
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
@Component({
selector: 'app-user-profile',
template: `
<div *ngIf="loading">Loading...</div>
<div *ngIf="user">
<h1>{{ user.name }}</h1>
<button (click)="logout()">Logout</button>
</div>
`
})
export class UserProfileComponent {
// β
Inject during field initialization
private http = inject(HttpClient);
private router = inject(Router);
user: any = null;
loading = false;
ngOnInit() {
// β
Use injected dependencies in lifecycle hooks
this.loadUser();
}
loadUser() {
this.loading = true;
// β
Use injected HttpClient
this.http.get('/api/user/profile').subscribe(user => {
this.user = user;
this.loading = false;
});
}
logout() {
// β
Use injected Router in event handler
this.router.navigate(['/login']);
}
}
Key Takeaways
π― Injection context is the "safe zone" where inject() can work
π― You're in an injection context during construction and initialization only
π― Inject early (field initializers), use later (lifecycle hooks, event handlers)
π― If you get an "inject() must be called in an injection context" error, you're trying to inject too late
π― The solution is almost always to move your inject() call to a field initializer
π‘ tip[!TIP] Easy Way to Remember: "CIFF vs LATE"
inject() works during CIFF:
- Constructor
- Initializers (field)
- Factory functions
- First-time setup
inject() fails during LATE:
- Lifecycle hooks
- Async callbacks
- Timers
- Event handlers
Simple Rule: Inject during CIFF, use during LATE!
β Section Recap: Injection Context
You just learned:
- β Injection context = where inject() can work (during initialization)
- β Works during: constructor, field initializers, factory functions
- β Doesn't work during: lifecycle hooks, events, async callbacks
- β Solution: "Inject early (CIFF), use later (LATE)"
- β
Always move
inject()calls to field initializers to avoid errors
Next up: Creating and using services in detail β
Understanding Providers
β±οΈ ~6 minutes
While providedIn: 'root' works great for most cases, sometimes you need more control. That's where manual provider configuration comes in.
When to Use Manual Providers
You should manually configure providers when:
- π§ The service doesn't have providedIn β Services without automatic provision must be manually provided
- π§ You want a new instance β Create a separate instance at the component level instead of using the shared singleton
- π§ You need runtime configuration β Service behavior depends on runtime values
- π§ You're providing non-class values β Configuration objects, functions, or primitive values
Provider Configuration Object
Every provider has two main parts:
- Provider identifier β The unique key (via the
provideproperty) - Value β The actual dependency, configured with:
useClassβ Provides a JavaScript classuseValueβ Provides a static valueuseFactoryβ Provides a factory function that returns the valueuseExistingβ Provides an alias to an existing provider
Provider Types with Examples
1. Class Providers (useClass)
When to use:
- β Swapping implementations (testing, A/B testing)
- β Providing a subclass instead of base class
- β Using different implementations per environment
Real example: Use MockAuthService in tests, RealAuthService in production
Don't use if: You just need a simple class β use providedIn: 'root' instead
import { Component } from '@angular/core';
import { Logger } from './logger.service';
import { BetterLogger } from './better-logger.service';
@Component({
selector: 'app-hero',
providers: [
{ provide: Logger, useClass: BetterLogger }
]
})
export class HeroComponent {
// This component will get BetterLogger when it asks for Logger
}
2. Value Providers (useValue)
When to use:
- β Providing configuration objects
- β Providing primitive values (strings, numbers, booleans)
- β Providing pre-existing object instances
Real example: API endpoints, feature flags, app configuration
Don't use if: You need dynamic behavior β use useFactory instead
const API_CONFIG = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
@Component({
selector: 'app-root',
providers: [
{ provide: 'API_CONFIG', useValue: API_CONFIG }
]
})
export class AppComponent {}
3. Factory Providers (useFactory)
When to use:
- β Creating services based on runtime conditions
- β Injecting dependencies into the factory
- β Complex initialization logic
Real example: Different logger based on environment, conditional service setup
Don't use if: Static values or simple classes work β simpler is better
function loggerFactory() {
return environment.production
? new ProductionLogger()
: new DevelopmentLogger();
}
@Component({
selector: 'app-root',
providers: [
{ provide: Logger, useFactory: loggerFactory }
]
})
export class AppComponent {}
4. Alias Providers (useExisting)
When to use:
- β Creating aliases for existing services
- β Backward compatibility (old code β new service)
- β Multiple tokens pointing to same instance
Real example: Deprecating old service name while maintaining compatibility
Don't use if: You need separate instances β use useClass instead
@Component({
providers: [
NewLogger,
{ provide: OldLogger, useExisting: NewLogger }
]
})
export class AppComponent {}
Hierarchical Dependency Injection
One of Angular's most powerful features is its hierarchical injector system. Understanding this hierarchy is key to mastering DI in Angular.
Two Injector Hierarchies
Angular has two parallel injector hierarchies:
1. EnvironmentInjector Hierarchy
- Platform Injector β Shared across all Angular applications on the page
- Root Environment Injector β Your main application injector
- Route Injectors β Created for lazy-loaded routes
Services provided with providedIn: 'root' or in ApplicationConfig providers live here.
2. ElementInjector Hierarchy
- Created for each component/directive in the component tree
- Mirrors the DOM structure of your application
- Services provided in component
providersarrays live here
How Dependencies Are Resolved
When Angular needs to resolve a dependency, it follows this process:
Resolution Steps:
- Angular first looks in the ElementInjector hierarchy (component tree)
- It searches up the component tree until it finds a provider
- If not found, it searches the EnvironmentInjector hierarchy
- If still not found, Angular throws an error
Important: The first provider Angular finds is the one it uses, even if there are other providers higher up the tree.
Resolution Modifiers
Angular provides special modifiers to control how dependencies are resolved. These are essential for building robust libraries or dealing with optional dependencies.
1. @Optional() or { optional: true }
Scenario: You have a feature that might not be enabled, like a premium badge.
π caution[!CAUTION] The Problem: Application Crash Trying to inject a service that hasn't been provided will cause your application to crash with a "No provider" error.
@Component({ ... })
export class UserCardComponent {
// π₯ CRASH: If PremiumBadgeService isn't provided, the app dies here.
private badgeService = inject(PremiumBadgeService);
hasPremiumBadge() {
return this.badgeService.isPremium();
}
}
π‘ tip[!TIP] The Solution: Graceful Fallback Use
@Optional()to tell Angular it's okay if the dependency is missing.
export class UserCardComponent {
// β
SAFE: Returns null if service is missing
private badgeService = inject(PremiumBadgeService, { optional: true });
hasPremiumBadge() {
// We must check if it exists before using it
return this.badgeService?.isPremium() ?? false;
}
}
2. @Self() or { self: true }
Scenario: A reusable component that must have its own local state.
π caution[!CAUTION] The Problem: Accidental Shared State If you forget to provide a service in a component that is meant to have its own local instance, it might silently find and share a parent's instance. This leads to weird bugs where two independent components update the same data.
@Component({
selector: 'app-file-uploader',
// π± OOPS: We forgot 'providers: [UploadStateService]'
template: `...`
})
export class FileUploaderComponent {
// We THINK we have our own state, but we're actually
// overwriting the state of a parent or sibling uploader!
private state = inject(UploadStateService);
}
π‘ tip[!TIP] The Solution: Enforce Local Instance Use
@Self()to force a local lookup. If you forgot to provide it, Angular will throw an error to alert you immediately.
@Component({
selector: 'app-file-uploader',
providers: [UploadStateService] // π @Self() forces us to have this!
})
export class FileUploaderComponent {
// β
SAFE: Guarantees this instance belongs ONLY to this component
private state = inject(UploadStateService, { self: true });
}
3. @SkipSelf() or { skipSelf: true }
Scenario: A component that needs to talk to a "parent" version of a service.
π caution[!CAUTION] The Problem: Shadowing Prevents Access Sometimes a component provides a service to configure its children, but it also needs to communicate with the global version of that same service. By default, it sees its own local instance.
@Component({
selector: 'app-toast-alert',
providers: [
// We provide a local generic config for children...
{ provide: ToastConfig, useValue: { position: 'bottom' } }
]
})
export class ToastAlertComponent {
// ...but we want to register with the GLOBAL tracker!
// β FAIL: This injects the local instance we might have implicitly provided or shadowed!
private globalTracker = inject(ToastTrackerService);
}
π‘ tip[!TIP] The Solution: Bypass Local Provider Use
@SkipSelf()to ignore the provider on the current component (the "Self") and start searching from the parent.
export class ToastAlertComponent {
// β
SUCCESS: Skips any local provider and finds the parent (global) tracker
private globalTracker = inject(ToastTrackerService, { skipSelf: true });
constructor() {
this.globalTracker.register(this);
}
}
4. @Host() or { host: true }
Scenario: A directive communicating with its host component.
π caution[!CAUTION] The Problem: Over-reaching Search Directives usually want to interact with the component they are attached to. Without limits, they might find a matching component generic component far up the DOM tree (like a grandparent).
@Directive({ selector: '[appTooltip]' })
export class TooltipDirective {
// We want the button this directive is on.
// β RISK: If this isn't on a button, it might keep searching up
// and find a 'ButtonComponent' wrapping the whole page!
private hostButton = inject(ButtonComponent);
}
π‘ tip[!TIP] The Solution: Restrict to Host Use
@Host()to tell Angular: "Only look at the element I'm attached to."
export class TooltipDirective {
// β
SAFE: Only looks at the element this directive is attached to.
// If the host isn't a ButtonComponent, it stops there (and errors or returns null).
private hostButton = inject(ButtonComponent, {
host: true,
optional: true
});
}
Combining Modifiers
You can mix and match these modifiers for powerful patterns. A common pattern in recursive components (like a tree menu) is to use @Optional() and @SkipSelf() to find a parent of the same type.
export class TreeItemComponent {
// Find a parent TreeItemComponent, if one exists (recursion!)
// If I am the root item, this will be null.
private parentItem = inject(TreeItemComponent, {
optional: true,
skipSelf: true
});
}
Component-Level Providers
You can provide services at the component level for specific use cases.
providers vs viewProviders
Angular components have two provider arrays:
providers Array
Makes services available to:
- The component itself
- All child components
- Content projected into the component (via
<ng-content>)
@Component({
selector: 'app-parent',
providers: [SharedService],
template: `
<app-child></app-child>
<ng-content></ng-content>
`
})
export class ParentComponent {}
viewProviders Array
Makes services available to:
- The component itself
- Child components in the template
- NOT available to projected content
@Component({
selector: 'app-parent',
viewProviders: [ViewOnlyService],
template: `<app-child></app-child>`
})
export class ParentComponent {}
Use Cases for Component Providers
Scenario 1: Service Isolation
Create independent instances for each component:
@Component({
selector: 'app-hero-editor',
providers: [HeroService] // Each editor gets its own HeroService
})
export class HeroEditorComponent {}
Scenario 2: Multiple Edit Sessions
Allow multiple instances of a component to maintain separate state:
@Component({
selector: 'app-form-editor',
providers: [FormStateService] // Each form has its own state
})
export class FormEditorComponent {}
InjectionToken for Non-Class Dependencies
When you need to inject values that aren't classes (like configuration objects or strings), use InjectionToken.
Creating an InjectionToken
import { InjectionToken } from '@angular/core';
export interface AppConfig {
apiUrl: string;
production: boolean;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
Providing the Token
const myConfig: AppConfig = {
apiUrl: 'https://api.example.com',
production: true
};
@Component({
providers: [
{ provide: APP_CONFIG, useValue: myConfig }
]
})
export class AppComponent {}
Injecting the Token
import { inject } from '@angular/core';
export class MyService {
private config = inject(APP_CONFIG);
getApiUrl() {
return this.config.apiUrl;
}
}
InjectionToken with providedIn
You can also provide default values directly in the token:
export const API_URL = new InjectionToken<string>('api.url', {
providedIn: 'root',
factory: () => 'https://api.example.com'
});
Advanced DI Patterns
Pattern 1: Forward References
Resolve circular dependencies using forwardRef:
import { forwardRef } from '@angular/core';
@Component({
providers: [
{
provide: ParentService,
useExisting: forwardRef(() => ChildService)
}
]
})
export class MyComponent {}
Pattern 2: Multi Providers
Provide multiple values for the same token:
export const PLUGIN_TOKEN = new InjectionToken<Plugin[]>('plugins');
@NgModule({
providers: [
{ provide: PLUGIN_TOKEN, useClass: Plugin1, multi: true },
{ provide: PLUGIN_TOKEN, useClass: Plugin2, multi: true }
]
})
export class AppModule {}
When injected, you get an array of all providers:
private plugins = inject(PLUGIN_TOKEN); // [Plugin1, Plugin2]
Pattern 3: Provide Functions (Library Pattern)
For library authors, create helper functions to provide services:
export function provideAnalytics(config: AnalyticsConfig) {
return [
{ provide: ANALYTICS_CONFIG, useValue: config },
AnalyticsService,
AnalyticsLogger
];
}
// Usage
bootstrapApplication(AppComponent, {
providers: [
provideAnalytics({ trackingId: 'UA-12345' })
]
});
Injection Context Best Practices
When You're in an Injection Context
The inject() function only works in certain places:
β
Constructor of @Injectable or @Component classes
β
Field initializers in these classes
β
Factory functions in providers
β
InjectionToken factory functions
Using inject() Outside a Context
If you need to use DI outside these contexts, use runInInjectionContext():
import { runInInjectionContext, inject } from '@angular/core';
class MyClass {
constructor(private injector: Injector) {}
doSomething() {
runInInjectionContext(this.injector, () => {
const service = inject(MyService);
service.doWork();
});
}
}
Real-World Example: Complete Feature
// 1. Define the service
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notifications$ = new BehaviorSubject<string[]>([]);
addNotification(message: string) {
const current = this.notifications$.value;
this.notifications$.next([...current, message]);
}
getNotifications() {
return this.notifications$.asObservable();
}
}
// 2. Create a configuration token
export const NOTIFICATION_CONFIG = new InjectionToken<NotificationConfig>(
'notification.config',
{
providedIn: 'root',
factory: () => ({ duration: 3000, position: 'top-right' })
}
);
// 3. Use in a component
@Component({
selector: 'app-notifications',
template: `
<div class="notification" *ngFor="let msg of notifications$ | async">
{{ msg }}
</div>
`
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
private config = inject(NOTIFICATION_CONFIG);
notifications$ = this.notificationService.getNotifications();
ngOnInit() {
console.log('Notification duration:', this.config.duration);
}
}
Common Pitfalls and How to Avoid Them
β Pitfall 1: Using inject() Outside Injection Context
Wrong:
export class MyComponent {
ngOnInit() {
const service = inject(MyService); // ERROR!
}
}
Correct:
export class MyComponent {
private service = inject(MyService); // β
Field initializer
ngOnInit() {
this.service.doSomething();
}
}
β Pitfall 2: Forgetting @Injectable Decorator
Wrong:
export class MyService { // Missing decorator!
doSomething() {}
}
Correct:
@Injectable({
providedIn: 'root'
})
export class MyService {
doSomething() {}
}
β Pitfall 3: Circular Dependencies
Problem: ServiceA depends on ServiceB, and ServiceB depends on ServiceA
Solution: Restructure your code or use forwardRef() if absolutely necessary
Performance Tips
1. Use providedIn: 'root' for Singletons
This enables tree-shaking and ensures optimal bundle size:
@Injectable({
providedIn: 'root' // β
Recommended
})
export class MyService {}
2. Provide Services Only Where Needed
If a service is only used in one module or component, provide it there:
@Component({
providers: [LocalOnlyService] // Only available to this component tree
})
export class FeatureComponent {}
3. Lazy Load with Route Providers
For feature-specific services, use route-level providers:
const routes: Routes = [
{
path: 'admin',
loadComponent: () => import('./admin.component'),
providers: [AdminService] // Only loads when route is accessed
}
];
---
## π§ Troubleshooting Guide
*Your DI error-solving toolkit*
Got an error? Follow this decision tree to solve common problems:
### β Error: "inject() must be called from an injection context"
**What it means**: You're calling `inject()` outside the initialization phase (during LATE, not CIFF).
**Quick fix**:
```typescript
// β Wrong - in lifecycle hook
ngOnInit() {
const service = inject(MyService); // ERROR!
}
// β
Correct - in field initializer
private service = inject(MyService); // Works!
Remember: Inject during CIFF, use during LATE!
β Error: "No provider for XyzService"
What it means: Angular can't find your service.
Checklist:
- β
Add
@Injectable()decorator to the service - β
Set
providedIn: 'root'in the decorator - β Check for typos in import path
- β
Verify service is in
providersarray (if not usingprovidedIn)
Quick fix:
@Injectable({ providedIn: 'root' }) // Add this!
export class MyService {}
β Error: "Circular dependency detected"
What it means: ServiceA β ServiceB β ServiceA
Solutions (in order):
- Best: Restructure - create a third service
- OK: Use
forwardRef(() => ServiceName) - Last resort: Inject lazily when needed
π Decision Tree
Got DI error?
β
ββ "inject() must be called from injection context"
β ββ β
Move inject() to field initializer
β
ββ "No provider for X"
β ββ Missing @Injectable()? β β
Add it
β ββ Missing providedIn? β β
Add 'root'
β ββ Typo in import? β β
Check path
β
ββ "Circular dependency"
β ββ β
Use forwardRef() or restructure
β
ββ Service not a singleton?
ββ β
Remove from component providers
β Scroll to view full diagram β
Conclusion
Dependency Injection is the backbone of Angular applications. By mastering DI, you can:
β
Write cleaner, more maintainable code
β
Build scalable applications with reusable services
β
Create testable components and services
β
Control service lifecycles and scopes precisely
β
Optimize your application's bundle size
Key Takeaways
- Use
providedIn: 'root'for most servicesβit's simple and enables tree-shaking - Inject with
inject()in field initializersβit's clean and modern - Understand the injector hierarchy to control where services come from
- Use resolution modifiers (
optional,self,skipSelf,host) when you need precise control - Leverage InjectionTokens for non-class dependencies like configuration objects
- Provide at the right levelβcomponent, route, or root depending on your needs
Going Further
Now that you understand Angular's DI system, you can:
- Build complex applications with confidence
- Design better service architectures
- Optimize your application's performance
- Write more effective unit tests
- Tackle advanced Angular patterns
Remember, Dependency Injection might seem complex at first, but with practice, it becomes second nature. Start simple with providedIn: 'root' and inject(), then gradually explore more advanced patterns as your needs grow!
π DI Cheat Sheet
Print this for quick reference!
Creating Services
@Injectable({ providedIn: 'root' }) // β
Most common
export class MyService { }
Injecting Services
// β
Modern (recommended)
private service = inject(MyService);
// β οΈ Old way (still works)
constructor(private service: MyService) {}
Provider Types
// Class
{ provide: Logger, useClass: BetterLogger }
// Value
{ provide: 'API_URL', useValue: 'https://api.com' }
// Factory
{ provide: Logger, useFactory: () => new Logger() }
// Alias
{ provide: OldService, useExisting: NewService }
Resolution Modifiers
private service = inject(MyService, { optional: true }); // Don't error if missing
private service = inject(MyService, { self: true }); // Only this component
private service = inject(MyService, { skipSelf: true }); // Skip this component
private service = inject(MyService, { host: true }); // Stop at host
Memory Aid: CIFF vs LATE
inject() works during CIFF:
- Constructor
- Initializers (field)
- Factory functions
- First-time setup
inject() fails during LATE:
- Lifecycle hooks
- Async callbacks
- Timers
- Event handlers
Common Errors & Fixes
| Error | Fix |
|---|---|
| inject() context error | Move to field initializer |
| No provider | Add @Injectable({ providedIn: 'root' }) |
| Circular dependency | Use forwardRef() or restructure |
| Not singleton | Remove from component providers |
π Bookmark this page for quick reference!
Happy coding! π
π§ Test Your Knowledge
Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.