One of the key architectural features of Angular is its Dependency Injection (DI) system. It allows developers to write modular, maintainable, and testable code by separating concerns. In Angular, services play a critical role in handling data, business logic, and shared functionality across different components. Components can easily access services through dependency injection.
This post will cover everything about injecting services into components—from basic examples to advanced concepts, including how dependency injection works under the hood, how to scope services properly, and how to design reusable and efficient services.
What Is a Service in Angular?
A service in Angular is a class that performs a specific task or encapsulates a particular piece of logic that you want to share across components. It can handle anything from data retrieval to state management or communication with APIs.
The key principle is separation of concerns — components should focus on the user interface and delegate other responsibilities (like fetching data) to services.
A simple service in Angular looks like this:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
private items = ['Apple', 'Banana', 'Cherry'];
getData(): string[] {
return this.items;
}
}
Here’s what happens in this code:
- The
@Injectable()
decorator marks the class as a service that can participate in Angular’s dependency injection system. - The
providedIn: 'root'
metadata means that this service is available application-wide as a singleton. - The
getData()
method returns a simple array of strings.
Injecting a Service into a Component
Once a service is defined, it can be injected into a component using Angular’s constructor injection pattern. This is one of the cleanest ways to use services inside components.
Example
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-demo',
template: <ul><li *ngFor="let item of data">{{ item }}</li></ul>
})
export class DemoComponent implements OnInit {
data: string[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
this.data = this.dataService.getData();
}
}
Explanation
- The service
DataService
is injected into the component’s constructor. - Angular automatically provides an instance of
DataService
when the component is created. - The
getData()
method is called insidengOnInit()
to initialize the component data. - The component template displays the data using the
*ngFor
directive.
This pattern makes the component’s logic much cleaner. Instead of directly managing data inside the component, it delegates that task to the service.
How Dependency Injection Works in Angular
Dependency injection in Angular is built around injectors, which are responsible for creating and delivering instances of classes (like services) to components or other services that need them.
When Angular creates a component, it checks the constructor parameters. If it finds a dependency (like private dataService: DataService
), it looks up a registered provider for that service. If a provider exists, Angular creates (or reuses) an instance of the service and injects it into the component.
The lifecycle of the service instance depends on where it is provided:
providedIn: 'root'
: Singleton service shared across the entire application.providedIn: SomeModule
: Scoped to a particular feature module.- Provided in
@Component.providers
: Scoped to a specific component and its child components.
Providing Services
1. Root-Level Services
By using providedIn: 'root'
, you make the service available everywhere. It’s the most common and recommended method because Angular’s tree-shaking mechanism removes unused services automatically.
@Injectable({
providedIn: 'root'
})
export class DataService { }
2. Module-Level Services
If you want to limit the scope of a service to a specific feature module:
@Injectable({
providedIn: SomeFeatureModule
})
export class FeatureService { }
Or manually provide it in a module:
@NgModule({
providers: [FeatureService]
})
export class FeatureModule { }
3. Component-Level Services
You can restrict the service to a specific component using the providers
array in the component’s decorator.
@Component({
selector: 'app-limited-scope',
template: <p>Component-specific service</p>
,
providers: [DataService]
})
export class LimitedScopeComponent {
constructor(private dataService: DataService) { }
}
Each instance of LimitedScopeComponent
gets its own service instance. This is useful for components that need isolated data or state.
Benefits of Injecting Services into Components
- Code Reusability: Services can be reused across multiple components.
- Separation of Concerns: Components handle the UI, while services handle business logic and data management.
- Maintainability: Logic changes can be made in one place (the service) without modifying multiple components.
- Testability: Mock services can easily replace real services in unit tests.
- Scalability: Helps in maintaining a clean architecture as the application grows.
Example: Using a Service to Fetch Data from an API
A common use of services is to interact with APIs. Let’s enhance our DataService
to make an HTTP request using Angular’s HttpClient
.
Setting Up the Service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private apiUrl = 'https://api.example.com/items';
constructor(private http: HttpClient) {}
getData(): Observable<string[]> {
return this.http.get<string[]>(this.apiUrl);
}
}
Using the Service in a Component
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-demo',
template: `
<ul>
<li *ngFor="let item of data">{{ item }}</li>
</ul>
`
})
export class DemoComponent implements OnInit {
data: string[] = [];
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.getData().subscribe(response => {
this.data = response;
});
}
}
Explanation
HttpClient
is injected into the service for making API requests.- The service returns an observable that emits the response.
- The component subscribes to this observable to get the data and display it.
This pattern keeps the component clean — it doesn’t know how or where the data comes from, just that it’s provided by the service.
Dependency Injection Hierarchy
Angular’s DI system supports hierarchical injectors, meaning that different parts of your application can have their own service instances.
Example Scenario
Suppose you have:
AppModule
providesDataService
.- A feature module provides the same
DataService
again.
Components under the feature module will receive a different instance of DataService
from those in the root module.
Visual Representation (Conceptually)
App Injector
├── DataService (root)
└── Feature Module Injector
└── DataService (scoped to feature)
This hierarchical design allows for isolated states and is useful for feature modules or lazy-loaded modules.
Service Injection in Other Services
Dependency injection isn’t limited to components. Services can inject other services too, enabling a powerful architecture.
Example:
@Injectable({
providedIn: 'root'
})
export class LoggerService {
log(message: string) {
console.log('Log:', message);
}
}
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private logger: LoggerService) {}
getData() {
this.logger.log('Fetching data...');
return ['Angular', 'React', 'Vue'];
}
}
Now, when getData()
is called, it logs a message before returning the data. This technique is perfect for separating cross-cutting concerns like logging, analytics, or caching.
Using Services for Shared State
You can use services to share data between unrelated components without using parent-child relationships.
Example:
@Injectable({
providedIn: 'root'
})
export class SharedService {
private message = '';
setMessage(msg: string) {
this.message = msg;
}
getMessage() {
return this.message;
}
}
Component A:
@Component({
selector: 'app-sender',
template: <button (click)="sendMessage()">Send Message</button>
})
export class SenderComponent {
constructor(private sharedService: SharedService) {}
sendMessage() {
this.sharedService.setMessage('Hello from Sender!');
}
}
Component B:
@Component({
selector: 'app-receiver',
template: <p>{{ message }}</p>
})
export class ReceiverComponent implements OnInit {
message = '';
constructor(private sharedService: SharedService) {}
ngOnInit() {
this.message = this.sharedService.getMessage();
}
}
The SharedService
acts as a bridge between SenderComponent
and ReceiverComponent
, allowing them to share data easily.
Using Observables in Services
For real-time updates, services can use RxJS observables.
@Injectable({
providedIn: 'root'
})
export class MessageService {
private messageSource = new BehaviorSubject<string>('Initial Message');
currentMessage = this.messageSource.asObservable();
changeMessage(message: string) {
this.messageSource.next(message);
}
}
Sender Component:
@Component({
selector: 'app-sender',
template: <button (click)="change()">Change Message</button>
})
export class SenderComponent {
constructor(private messageService: MessageService) {}
change() {
this.messageService.changeMessage('Message updated!');
}
}
Receiver Component:
@Component({
selector: 'app-receiver',
template: <p>{{ message }}</p>
})
export class ReceiverComponent implements OnInit {
message = '';
constructor(private messageService: MessageService) {}
ngOnInit() {
this.messageService.currentMessage.subscribe(msg => this.message = msg);
}
}
This reactive pattern allows multiple components to stay synchronized without direct connections.
Testing Components with Injected Services
Since services are injected, it’s easy to mock them during testing.
Example unit test:
import { TestBed } from '@angular/core/testing';
import { DemoComponent } from './demo.component';
import { DataService } from './data.service';
describe('DemoComponent', () => {
let component: DemoComponent;
let dataServiceSpy: jasmine.SpyObj<DataService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('DataService', ['getData']);
TestBed.configureTestingModule({
declarations: [DemoComponent],
providers: [{ provide: DataService, useValue: spy }]
});
const fixture = TestBed.createComponent(DemoComponent);
component = fixture.componentInstance;
dataServiceSpy = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
});
it('should load data from the service', () => {
const mockData = ['Item 1', 'Item 2'];
dataServiceSpy.getData.and.returnValue(mockData);
component.ngOnInit();
expect(component.data).toEqual(mockData);
});
});
Common Mistakes When Injecting Services
- Forgetting to Decorate with
@Injectable()
Without the decorator, Angular cannot inject dependencies into the service. - Providing the Same Service at Multiple Levels Unintentionally
This can create multiple instances of the same service. - Circular Dependency
When two services depend on each other, Angular cannot resolve them. Refactor to break the circular reference. - Not Unsubscribing from Observables
If a service provides a stream that components subscribe to, make sure to unsubscribe on component destruction.
Leave a Reply