Dependency Injection
Dependency Injection (DI) is a core feature in Angular that enables dynamic resolution of dependencies between components and services at runtime, rather than hardcoding them at compile time. DI enhances code testability, maintainability, and promotes loose coupling and reusability.
Basic Concepts
- Dependency: An object relies on another object to provide specific services or information to function correctly.
- Injection: Dependency injection delegates the resolution of dependencies to an external container (in Angular, the framework itself) rather than the object itself.
- Service: In Angular, services are classes that provide specific functionality, such as data access, logging, or configuration management, and are used via dependency injection.
How Angular DI Works
Angular’s DI system revolves around the concept of a “Provider,” an object that instructs Angular on how to create and provide dependencies. Every Angular application has a root injector created at startup, with all other injectors being its descendants.
Provider Types:
- Class Provider: Specifies a class as the dependency implementation.
- Value Provider: Provides a specific value for the dependency.
- Factory Provider: Uses a factory function to create the dependency.
- Injection Tokens: Identifies dependencies using a class, string, or symbol.
Configuring DI
Register a service in AppModule:
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { MyService } from './my.service';
@NgModule({
declarations: [
AppComponent
],
imports: [
// Other modules
],
providers: [
MyService
],
bootstrap: [AppComponent]
})
export class AppModule { }Injecting a Service in a Component:
import { Component } from '@angular/core';
import { MyService } from './my.service';
@Component({
selector: 'app-root',
template: `
<h1>Hello, {{ message }}</h1>
`,
})
export class AppComponent {
message: string;
constructor(private myService: MyService) {
this.message = this.myService.getMessage();
}
}Module-Level DI
Services can be registered in any module to organize and reuse code effectively.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MyService } from './my.service';
@NgModule({
imports: [
CommonModule
],
providers: [
MyService
]
})
export class SharedModule { }Hierarchical Injectors
Angular supports hierarchical injectors, allowing dependency registration at different levels (root module, submodules, components), with each level potentially overriding previous configurations.
Scopes
Service instances can have different lifecycles based on their scope:
- Singleton: A single instance exists for the entire application.
- Prototype: A new instance is created for each request.
By default, Angular services are singletons.
Best Practices for Dependency Injection
- Use Services: Encapsulate business logic in services, not components.
- Avoid Circular Dependencies: Ensure dependencies do not form a loop.
- Use Factory Functions: Employ factory functions for complex dependency creation.
- Avoid Over-Injection: Inject only the services a component needs.
Testing with DI
DI simplifies unit testing by allowing easy replacement or mocking of dependencies.
Using TestBed:
import { TestBed } from '@angular/core/testing';
import { MyService } from './my.service';
describe('MyService', () => {
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MyService]
});
service = TestBed.inject(MyService);
});
it('should return correct message', () => {
expect(service.getMessage()).toBe('Hello, world!');
});
});Custom Providers
Custom providers allow control over service instance creation, useful for services requiring special initialization.
Factory Provider Example:
providers: [
{
provide: MyService,
useFactory: (dependencyA: DependencyA) => {
return new MyService(dependencyA);
},
deps: [DependencyA]
}
]The factory function creates a MyService instance, receiving DependencyA as a parameter.
Injection Tokens
Injection tokens uniquely identify dependencies, using classes, strings, or InjectionToken (replacing OpaqueToken in Angular 4+).
import { InjectionToken } from '@angular/core';
export const MY_CONFIG = new InjectionToken('MyConfig');
@NgModule({
providers: [
{ provide: MY_CONFIG, useValue: { apiUrl: 'https://api.example.com' } }
]
})
export class AppModule { }Injecting the Configuration:
constructor(@Inject(MY_CONFIG) private config: any) {
console.log(this.config.apiUrl); // Outputs: https://api.example.com
}Lazy-Loaded Services
In large applications, lazy-loaded services optimize performance by loading only when needed, typically within lazy-loaded modules.
const routes: Routes = [
{
path: 'lazy',
loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
}
];Services defined in lazy-loaded modules are loaded and injected only when the module is accessed.
Advanced Injection Strategies
- Optional: Marks a dependency as optional, injecting
nullif unresolved.
constructor(@Optional() dependency?: SomeService) { }- Host: Retrieves dependencies from the host element or component, not the injector.
constructor(@Host() dependency: SomeService) { }- Self: Looks for dependencies only in the current component’s injector.
constructor(@Self() dependency: SomeService) { }- SkipSelf: Retrieves dependencies from parent injectors, skipping the current injector.
constructor(@SkipSelf() dependency: SomeService) { }Advanced Usage: Multi Providers
Multi providers allow multiple implementations for the same token, useful for services like logging with multiple handlers.
@NgModule({
providers: [
{ provide: LOGGER, useClass: ConsoleLogger, multi: true },
{ provide: LOGGER, useClass: FileLogger, multi: true }
]
})
export class AppModule { }Consuming Multi Providers:
constructor(@Inject(LOGGER) loggers: Logger[]) {
loggers.forEach(logger => logger.log('Logging...'));
}Services
Angular services encapsulate business logic, data access, and state management, reusable across components via dependency injection, enhancing maintainability and testability.
Creating a Service
Services are classes with business logic, injectable into components or other services.
// services/my-service.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // Or 'any' or a specific module
})
export class MyService {
constructor() { }
getData(): string {
return 'Hello from MyService!';
}
}The @Injectable() decorator marks the service for dependency injection, with providedIn specifying its scope.
Injecting a Service
Inject a service into a component by declaring it in the constructor.
// components/app.component.ts
import { Component } from '@angular/core';
import { MyService } from './services/my-service.service';
@Component({
selector: 'app-root',
template: `<h1>{{ data }}</h1>`
})
export class AppComponent {
data: string;
constructor(private myService: MyService) {
this.data = this.myService.getData();
}
}Inter-Service Communication
Services can depend on each other, communicating via dependency injection.
// services/data.service.ts
@Injectable({ providedIn: 'root' })
export class DataService {
private data = 'Initial Data';
setData(newData: string) {
this.data = newData;
}
getData() {
return this.data;
}
}
// services/log.service.ts
@Injectable({ providedIn: 'root' })
export class LogService {
constructor(private dataService: DataService) {}
logData() {
console.log(this.dataService.getData());
}
}Service Lifecycle
Angular’s dependency injection system manages service instantiation and destruction. By default, services are singletons, with one instance per application.
Testing Services
Service tests often use mocks to isolate dependencies.
// services/my-service.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { MyService } from './my-service.service';
describe('MyService', () => {
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(MyService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return correct data', () => {
expect(service.getData()).toEqual('Hello from MyService!');
});
});Services and State Management
For complex state management, use NgRx or RxJS’s BehaviorSubject.
// services/state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class StateService {
private state$ = new BehaviorSubject<{ count: number }>({ count: 0 });
getState() {
return this.state$.asObservable();
}
updateState(newCount: number) {
this.state$.next({ count: newCount });
}
}Subscribing to State in a Component:
// components/app.component.ts
import { Component, OnInit } from '@angular/core';
import { StateService } from './services/state.service';
@Component({
selector: 'app-root',
template: `<h1>Count: {{ count }}</h1>`
})
export class AppComponent implements OnInit {
count: number;
constructor(private stateService: StateService) {}
ngOnInit() {
this.stateService.getState().subscribe(state => {
this.count = state.count;
});
}
}Services and Network Requests
Services often encapsulate HTTP requests using Angular’s HttpClient module.
// services/http.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class HttpService {
constructor(private http: HttpClient) {}
fetchData() {
return this.http.get('https://api.example.com/data');
}
}Services and Error Handling
Services can centralize HTTP error handling for consistent responses.
// services/error-handler.service.ts
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
handle(errorResponse: HttpErrorResponse) {
console.error('Error:', errorResponse);
}
}HTTP Client and Angular HttpClient
Angular’s HTTP client API, primarily the HttpClient module, facilitates interaction with web servers. It supports HTTP methods like GET, POST, PUT, and DELETE, and integrates with RxJS for seamless asynchronous data handling.
Importing HttpClient
To use HttpClient, import HttpClientModule in the module.
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule // Import HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }Using HttpClient
Inject HttpClient into a service to make HTTP requests.
// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) { }
getData() {
return this.http.get('https://api.example.com/data');
}
postData(data: any) {
return this.http.post('https://api.example.com/data', data);
}
}Handling Responses
HttpClient methods return RxJS Observable objects, requiring subscription to access results.
// components/app.component.ts
import { Component, OnInit } from '@angular/core';
import { ApiService } from './services/api.service';
@Component({
selector: 'app-root',
template: `
<h1>Data from Server:</h1>
<pre>{{ data | json }}</pre>
`
})
export class AppComponent implements OnInit {
data: any;
constructor(private apiService: ApiService) { }
ngOnInit() {
this.apiService.getData()
.subscribe(response => {
this.data = response;
}, error => {
console.error('Error fetching data:', error);
});
}
}Error Handling
Handle HTTP errors using RxJS’s catchError operator.
import { catchError } from 'rxjs/operators';
getData() {
return this.http.get('https://api.example.com/data')
.pipe(
catchError(this.handleError)
);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error);
return Promise.reject(error.message || error);
}Configuring Request Headers
Add custom headers, such as Content-Type or authentication tokens, to requests.
import { HttpHeaders } from '@angular/common/http';
postData(data: any) {
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
return this.http.post('https://api.example.com/data', data, { headers });
}Using JSON Data
HttpClient automatically handles JSON serialization and deserialization.
getData() {
return this.http.get<any>('https://api.example.com/data');
}
postData(data: any) {
return this.http.post<any>('https://api.example.com/data', data);
}Uploading Files
Use FormData to upload files.
uploadFile(file: File) {
const formData = new FormData();
formData.append('file', file, file.name);
return this.http.post('https://api.example.com/upload', formData);
}Testing HttpClient
Use HttpClientTestingModule to mock HttpClient in unit tests.
import { HttpClientTestingModule } from '@angular/common/http/testing';
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [ApiService]
});
});Advanced Topics
- Interceptors: Use
HttpInterceptorto intercept and modify requests or responses. - Canceling Requests: Use
AbortControllerto cancel ongoing HTTP requests. - Response Types: Specify response types like
text,json, orblob.
Signals
In Angular, “signals” primarily relate to RxJS, a reactive programming library providing Observables and Observers. Since Angular 14, a new reactive programming tool called Signals was introduced, offering a simpler, more intuitive state management approach. Signals are observable, updatable data structures for storing and sharing application state.
Signal Basics
Signals are reactive references that can be subscribed to and updated anywhere. They always have a current value, and subscribers are notified of changes.
import { signal } from '@angular/core';
class App {
private _count = signal(0);
get count() {
return this._count();
}
increment() {
this._count.set(this.count + 1);
}
}Creating Signals
Create a signal with the signal function, passing an initial value.
import { signal } from '@angular/core';
const name = signal('Alice');Accessing and Updating Signals
Access a signal’s value by calling the signal function and update it with .set.
name(); // Get current value
name.set('Bob'); // Update valueSubscribing to Signals
Subscribe to signal changes with .subscribe.
name.subscribe(value => {
console.log(`Name changed to ${value}`);
});Signals and Components
Use signals as component properties, exposing them to templates.
import { Component } from '@angular/core';
import { signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>Welcome, {{ name() }}</h1>
<button (click)="changeName()">Change Name</button>
`
})
export class AppComponent {
name = signal('Alice');
changeName() {
this.name.set('Bob');
}
}Signals and Services
Signals in services enable state sharing across components.
// services/state.service.ts
import { Injectable } from '@angular/core';
import { signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StateService {
public name = signal('Alice');
}Using the Signal in a Component:
// components/app.component.ts
import { Component } from '@angular/core';
import { StateService } from './services/state.service';
@Component({
selector: 'app-root',
template: `
<h1>Welcome, {{ stateService.name() }}</h1>
<button (click)="stateService.name.set('Bob')">Change Name</button>
`
})
export class AppComponent {
constructor(public stateService: StateService) {}
}Signals and Forms
Signals can be used for two-way binding in form controls.
// components/form.component.ts
import { Component } from '@angular/core';
import { signal } from '@angular/core';
@Component({
selector: 'app-form',
template: `
<input [(ngModel)]="username">
`
})
export class FormComponent {
username = signal('');
}Signals and Routing
Signals can respond to route changes, such as updating the page title.
// app.component.ts
import { Component } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter, map } from 'rxjs/operators';
import { signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<title>{{ title() }}</title>
`
})
export class AppComponent {
title = signal('Home');
constructor(private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.router.url)
).subscribe(url => {
if (url === '/about') {
this.title.set('About Us');
} else {
this.title.set('Home');
}
});
}
}Signals and Performance Optimization
Signals optimize performance by avoiding unnecessary renders when their values remain unchanged.
Signals and Testing
Signals can be tested by simulating state changes with .set, enabling predictable and isolated tests.
it('should change name', () => {
const stateService = TestBed.inject(StateService);
stateService.name.set('Charlie');
expect(stateService.name()).toBe('Charlie');
});



