Angular services are one of the most powerful features of the framework, allowing developers to share logic, manage state, and organize functionality in a structured way. Typically, services are provided at the root level, making them available throughout the entire application.
However, there are cases when each component should have its own independent instance of a service.
This is where component-level services come into play.
In this detailed guide, we’ll explore what component-level services are, why and when to use them, how they differ from global services, and how to build real-world examples demonstrating isolated component states.
Understanding Service Scopes in Angular
Before diving into component-level services, it’s important to understand the scope of services in Angular — that is, how and where a service instance is created and shared.
There are three main ways to provide a service in Angular:
- Root-level services – Shared globally throughout the app.
- Module-level services – Scoped to a specific module.
- Component-level services – Scoped to an individual component and its descendants.
Each scope affects how instances are created and reused.
1. Root-Level Services
When a service is provided at the root level:
@Injectable({
providedIn: 'root'
})
- Angular creates a single, shared instance for the entire application.
- All components using this service share the same data and state.
- It’s useful for global data, such as authentication, user preferences, or configuration.
2. Module-Level Services
When provided in a feature module, the service instance is created for that module only.
@NgModule({
providers: [SomeService]
})
export class FeatureModule {}
- Components in the same module share the instance.
- Different modules can have separate instances of the same service.
- This is ideal for lazy-loaded modules that need their own isolated state.
3. Component-Level Services
When provided directly in a component’s @Component
decorator:
@Component({
selector: 'app-child',
template: {{ counterService.count }}
,
providers: [CounterService]
})
export class ChildComponent {}
- Each instance of the component gets its own unique instance of the service.
- The state is not shared across sibling or parent components.
- This is particularly useful when local state management is required.
Why Use Component-Level Services?
Global (root-level) services are convenient, but sometimes sharing state across components can cause problems. Component-level services solve this by isolating state per component instance.
Key Advantages:
- Encapsulation of State
- Each component has its own service instance, so data stays local.
- Perfect for components that should not affect each other’s state.
- Simplified Lifecycle
- When the component is destroyed, its service instance is also destroyed automatically.
- Avoiding Shared-State Bugs
- Prevents unintended interactions between multiple instances of a component.
- Scoped Logic
- Keeps logic tightly coupled to the component it serves.
- Improved Testability
- Each test instance of the component gets a clean service instance.
Step 1: Creating a Simple Counter Service
Let’s start by creating a simple service that holds a counter value.
import { Injectable } from '@angular/core';
@Injectable()
export class CounterService {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
reset() {
this.count = 0;
}
}
This CounterService
maintains a single property count
, and provides methods to modify it.
Notice that this service does not specify providedIn: 'root'
.
That’s because we want to provide it manually at the component level.
Step 2: Providing the Service in a Component
Now, let’s create a child component that uses this service.
import { Component } from '@angular/core';
import { CounterService } from '../counter.service';
@Component({
selector: 'app-child',
template: `
<h3>Child Component</h3>
<p>Count: {{ counterService.count }}</p>
<button (click)="counterService.increment()">Increment</button>
<button (click)="counterService.decrement()">Decrement</button>
<button (click)="counterService.reset()">Reset</button>
`,
providers: [CounterService]
})
export class ChildComponent {
constructor(public counterService: CounterService) {}
}
Key Observations:
- The
providers
array is defined in the@Component
decorator. - Angular creates a new instance of
CounterService
for eachChildComponent
. - Each child component manages its own counter value independently.
- This instance is destroyed when the component is destroyed.
Step 3: Using Multiple Component Instances
Now, let’s use multiple instances of this component in a parent component.
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<h2>Parent Component</h2>
<app-child></app-child>
<app-child></app-child>
`
})
export class ParentComponent {}
What Happens Here:
- Each
<app-child>
gets a separate instance ofCounterService
. - Incrementing or decrementing the count in one child does not affect the other.
This behavior demonstrates true isolation of state between component instances.
Step 4: Comparing Root-Level vs Component-Level Service
Let’s compare both approaches side by side.
Feature | Root-Level Service | Component-Level Service |
---|---|---|
Number of Instances | One (Singleton) | One per component |
State Sharing | Shared globally | Isolated per component |
Memory Use | Lower (single instance) | Slightly higher |
Lifecycle | Persists for app lifetime | Tied to component |
Best For | Shared data (e.g., user info, config) | Localized state (e.g., counters, UI data) |
Step 5: Visualizing Component-Level Service Behavior
Imagine two ChildComponent
instances on the screen:
- Child A → has its own
CounterService
instance. - Child B → has its own
CounterService
instance.
When you increment the count in Child A, only its counter changes.
Child B’s counter remains unaffected because it has a separate service instance.
This isolation ensures modular, predictable behavior — ideal for scenarios where shared state can cause confusion or bugs.
Step 6: How Angular Handles Service Instantiation
Angular uses a hierarchical dependency injection (DI) system.
When Angular looks for a service instance:
- It first checks the component’s own providers.
- If not found, it checks the parent component’s providers.
- Then it goes up to module providers.
- Finally, it checks root-level providers.
So when you specify:
providers: [CounterService]
inside a component, Angular creates a new instance right there in the component’s injector, instead of reusing one from higher up in the hierarchy.
Step 7: Example of Shared vs Isolated Instances
Consider two scenarios:
Scenario 1: Provided in Root
@Injectable({
providedIn: 'root'
})
export class CounterService {
count = 0;
}
Both components share the same count value.
Scenario 2: Provided in Component
@Component({
selector: 'app-child',
providers: [CounterService]
})
Each component has its own count value.
Changing one does not affect the other.
Step 8: Parent-Child Service Relationship
Sometimes, you might want a service to be shared only between a parent component and its direct children.
To do this, provide the service in the parent component, not globally or per child.
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
<app-child></app-child>
`,
providers: [CounterService]
})
export class ParentComponent {}
- The
CounterService
instance is shared between the parent and both children. - It is not shared outside this parent component.
- This allows “grouped state” sharing without affecting the rest of the app.
Step 9: Component-Level Lifecycle
When a service is provided at the component level:
- It is created when the component is instantiated.
- It is destroyed when the component is destroyed.
This makes memory management cleaner and more predictable.
Once the component disappears, the service and its state disappear too.
Example:
@Component({
selector: 'app-timer',
providers: [TimerService]
})
export class TimerComponent implements OnDestroy {
constructor(private timer: TimerService) {}
ngOnDestroy() {
console.log('TimerComponent destroyed');
}
}
Here, when TimerComponent
is removed from the DOM, its TimerService
instance is also removed automatically.
Step 10: Real-World Use Cases
1. Independent Widgets
Each widget, such as a chart, table, or counter, can manage its own state through its component-level service.
2. Dialogs or Modals
Each dialog instance can use its own service to manage data without affecting other dialogs.
3. Temporary Calculations
If the data is temporary and relevant only to one component, a local service helps keep logic isolated.
4. Testing Environments
Component-level services are great for isolated unit tests, since they automatically reset with the component.
Step 11: Advanced Example — Local Shopping Cart Component
Imagine each product card has its own mini “cart” feature.
Each card can add or remove items independently.
cart.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class CartService {
items: string[] = [];
addItem(item: string) {
this.items.push(item);
}
removeItem(item: string) {
this.items = this.items.filter(i => i !== item);
}
getItems() {
return this.items;
}
}
product.component.ts
import { Component } from '@angular/core';
import { CartService } from '../cart.service';
@Component({
selector: 'app-product',
template: `
<h4>{{ productName }}</h4>
<button (click)="add()">Add</button>
<button (click)="remove()">Remove</button>
<p>Cart Items: {{ cartService.getItems().length }}</p>
`,
providers: [CartService]
})
export class ProductComponent {
productName = 'Laptop';
constructor(public cartService: CartService) {}
add() {
this.cartService.addItem(this.productName);
}
remove() {
this.cartService.removeItem(this.productName);
}
}
Each product component now has its own isolated cart service — independent from other products.
Step 12: When Not to Use Component-Level Services
While component-level services are powerful, they’re not always the right choice.
Avoid using them when:
- You need shared state across multiple parts of the app.
- You need persistent data that survives component destruction.
- You want to centralize logic for reuse across modules.
In such cases, prefer root-level or module-level services.
Step 13: Testing Component-Level Services
Testing becomes easier with component-level services because every component instance gets a fresh service.
Example Unit Test:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChildComponent } from './child.component';
import { CounterService } from '../counter.service';
describe('ChildComponent with Component-Level Service', () => {
let fixture: ComponentFixture<ChildComponent>;
let component: ChildComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ChildComponent],
providers: [] // Service is provided by component itself
});
fixture = TestBed.createComponent(ChildComponent);
component = fixture.componentInstance;
});
it('should create a new service instance per component', () => {
const serviceA = fixture.componentRef.injector.get(CounterService);
const newFixture = TestBed.createComponent(ChildComponent);
const serviceB = newFixture.componentRef.injector.get(CounterService);
expect(serviceA).not.toBe(serviceB);
});
});
Each test confirms that a new instance of the service is created per component.
Step 14: Common Mistakes
- Using providedIn: ‘root’ unintentionally
- If you want a local instance, never use
providedIn: 'root'
in your service.
- If you want a local instance, never use
- Providing the same service in multiple places
- Providing it in both the component and module can create multiple unexpected instances.
- Expecting shared state
- Remember that component-level services do not share state with siblings.
- Not cleaning up subscriptions
- Although services are destroyed with the component, always unsubscribe from Observables in the component itself for best practice.
Step 15: Performance Considerations
Component-level services slightly increase memory usage since each component instance gets its own copy.
However, this overhead is minimal for most apps and is often worth it for better isolation and maintainability.
Angular efficiently manages these instances — destroying them when the component is removed from the DOM.
Step 16: Combining Strategies
In some applications, you may combine global and local services.
Example:
- A global
UserService
holds the logged-in user. - A component-level
SettingsService
handles user preferences for a specific panel.
This hybrid approach balances shared and isolated state.
Step 17: Real-World Analogy
Think of global services like a shared public library — one library, many users.
In contrast, component-level services are like private notebooks — each person has their own copy and notes.
Both are useful in different contexts.
Step 18: Summary of Component-Level Service Behavior
Aspect | Description |
---|---|
Scope | Limited to the component and its children |
Lifecycle | Created when component is instantiated; destroyed when removed |
Instances | One per component |
Use Case | Local, isolated state or logic |
Sharing | Not shared across siblings |
Definition | Declared in the component’s providers array |
Step 19: Full Working Example Recap
counter.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class CounterService {
count = 0;
increment() { this.count++; }
decrement() { this.count--; }
reset() { this.count = 0; }
}
child.component.ts
import { Component } from '@angular/core';
import { CounterService } from '../counter.service';
@Component({
selector: 'app-child',
template: `
<h4>Child Component</h4>
<p>Count: {{ counterService.count }}</p>
<button (click)="counterService.increment()">+</button>
<button (click)="counterService.decrement()">-</button>
<button (click)="counterService.reset()">Reset</button>
`,
providers: [CounterService]
})
export class ChildComponent {
constructor(public counterService: CounterService) {}
}
parent.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<h2>Parent Component</h2>
<app-child></app-child>
<app-child></app-child>
`
})
export class ParentComponent {}
Each <app-child>
maintains its own independent counter instance.
Leave a Reply