Component Level Service in Angular

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:

  1. Root-level services – Shared globally throughout the app.
  2. Module-level services – Scoped to a specific module.
  3. 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:

  1. 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.
  2. Simplified Lifecycle
    • When the component is destroyed, its service instance is also destroyed automatically.
  3. Avoiding Shared-State Bugs
    • Prevents unintended interactions between multiple instances of a component.
  4. Scoped Logic
    • Keeps logic tightly coupled to the component it serves.
  5. 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 each ChildComponent.
  • 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 of CounterService.
  • 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.

FeatureRoot-Level ServiceComponent-Level Service
Number of InstancesOne (Singleton)One per component
State SharingShared globallyIsolated per component
Memory UseLower (single instance)Slightly higher
LifecyclePersists for app lifetimeTied to component
Best ForShared 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:

  1. It first checks the component’s own providers.
  2. If not found, it checks the parent component’s providers.
  3. Then it goes up to module providers.
  4. 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: `
&lt;app-child&gt;&lt;/app-child&gt;
&lt;app-child&gt;&lt;/app-child&gt;
`, 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 =&gt; i !== item);
} getItems() {
return this.items;
} }

product.component.ts

import { Component } from '@angular/core';
import { CartService } from '../cart.service';

@Component({
  selector: 'app-product',
  template: `
&lt;h4&gt;{{ productName }}&lt;/h4&gt;
&lt;button (click)="add()"&gt;Add&lt;/button&gt;
&lt;button (click)="remove()"&gt;Remove&lt;/button&gt;
&lt;p&gt;Cart Items: {{ cartService.getItems().length }}&lt;/p&gt;
`, 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:

  1. You need shared state across multiple parts of the app.
  2. You need persistent data that survives component destruction.
  3. 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: &#91;ChildComponent],
  providers: &#91;] // 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

  1. Using providedIn: ‘root’ unintentionally
    • If you want a local instance, never use providedIn: 'root' in your service.
  2. Providing the same service in multiple places
    • Providing it in both the component and module can create multiple unexpected instances.
  3. Expecting shared state
    • Remember that component-level services do not share state with siblings.
  4. 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

AspectDescription
ScopeLimited to the component and its children
LifecycleCreated when component is instantiated; destroyed when removed
InstancesOne per component
Use CaseLocal, isolated state or logic
SharingNot shared across siblings
DefinitionDeclared 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: `
&lt;h4&gt;Child Component&lt;/h4&gt;
&lt;p&gt;Count: {{ counterService.count }}&lt;/p&gt;
&lt;button (click)="counterService.increment()"&gt;+&lt;/button&gt;
&lt;button (click)="counterService.decrement()"&gt;-&lt;/button&gt;
&lt;button (click)="counterService.reset()"&gt;Reset&lt;/button&gt;
`, providers: [CounterService] }) export class ChildComponent { constructor(public counterService: CounterService) {} }

parent.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-parent',
  template: `
&lt;h2&gt;Parent Component&lt;/h2&gt;
&lt;app-child&gt;&lt;/app-child&gt;
&lt;app-child&gt;&lt;/app-child&gt;
` }) export class ParentComponent {}

Each <app-child> maintains its own independent counter instance.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *