Angular Dependency Injection Fundamentals: Services, inject() & Injection Context
Learn Angular's Dependency Injection from scratch. Understand services, the inject() function, injection context, and the CIFF vs LATE rule with practical examples.
- 1. Angular Dependency Injection Fundamentals: Services, inject() & Injection Context
- 2. Angular Dependency Injection Advanced: Providers, Hierarchical DI & Resolution Modifiers
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: ~12 minutes
Skill Level: Beginner β Intermediate
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
π‘ New to Angular DI? Start from the top and work your way down.
π― Debugging an error? Jump to Injection Context.
π Ready for advanced topics? Check out Part 2: Providers, Hierarchical DI & Resolution Modifiers.
What You'll Learn
In this guide (Part 1 of 2), we'll explore Angular's Dependency Injection system from the ground up. This part covers the foundational concepts every Angular developer needs to master.
Topics Covered:
- β What Dependency Injection is and why it matters
- β How to create and use services effectively
- β
The modern
inject()function and how to use it - β
Injection contexts and when you can use
inject() - β The CIFF vs LATE rule for avoiding common errors
Coming in Part 2:
- π Understanding providers and different provider types
- π Mastering hierarchical injection and injector trees
- π Using resolution modifiers to control dependency lookup
- π Advanced patterns, real-world use cases, and troubleshooting
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
π What's Next?
You've mastered the fundamentals of Angular Dependency Injection! You now understand services, the inject() function, and injection context.
In Part 2: Providers, Hierarchical DI & Resolution Modifiers, we'll dive into the advanced side:
- π§ Provider types β
useClass,useValue,useFactory,useExisting - π³ Hierarchical DI β How Angular's injector tree works
- π― Resolution modifiers β
@Optional(),@Self(),@SkipSelf(),@Host() - π InjectionTokens β Injecting non-class dependencies
- π§© Advanced patterns β Multi providers, forward references, and real-world architecture
- π§ Troubleshooting guide β Decision tree for common DI errors