Angular Services & Dependency Injection

What is a Service?

A service is a TypeScript class that contains logic or data that can be shared across multiple components. In Angular, the principle is to keep components focused on displaying data and handling user interactions, while all business logic, data fetching, and shared state live in services.

Without services, every component that needs the same data would have to duplicate the same logic. Services solve this by centralizing the logic in one place. Any component that needs it simply receives the service through Angular's dependency injection system.

When to Use a Service

A service is the right choice when:

  • Multiple components need access to the same data or functionality.
  • Code handles API calls or HTTP requests.
  • Shared state needs to be maintained between components.
  • Complex business logic would clutter a component class.
  • Utility functions are needed across the application.

What is Dependency Injection?

Dependency Injection (DI) is a design pattern where a class receives its dependencies from an external source rather than creating them itself. In Angular, the framework automatically creates service instances and provides ("injects") them to any class that needs them.

Think of it like ordering food at a restaurant. The customer (component) does not go into the kitchen and cook the food (create the service). Instead, the waiter (Angular's injector) brings the food to the table. The component just asks for what it needs, and Angular provides it.

Creating a Service with the Angular CLI

Generate a new service using the CLI:


ng generate service services/product

This creates two files:


src/app/services/
├── product.service.ts
└── product.service.spec.ts

Anatomy of a Service


// product.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'   // Makes this service available throughout the application
})
export class ProductService {

  private products = [
    { id: 1, name: 'Laptop Stand', price: 39.99, category: 'Accessories' },
    { id: 2, name: 'Wireless Mouse', price: 24.99, category: 'Accessories' },
    { id: 3, name: 'USB-C Hub', price: 45.00, category: 'Accessories' },
    { id: 4, name: 'Monitor 27"', price: 299.99, category: 'Displays' }
  ];

  getAllProducts() {
    return this.products;
  }

  getProductById(id: number) {
    return this.products.find(p => p.id === id);
  }

  getProductsByCategory(category: string) {
    return this.products.filter(p => p.category === category);
  }

  addProduct(product: { id: number; name: string; price: number; category: string }) {
    this.products.push(product);
  }

  getTotalValue(): number {
    return this.products.reduce((sum, p) => sum + p.price, 0);
  }
}

The @Injectable Decorator

The @Injectable decorator marks this class as a service that Angular's dependency injection system can manage. Without this decorator, Angular cannot inject the class into other classes.

providedIn: 'root'

Setting providedIn: 'root' tells Angular to create a single shared instance of this service for the entire application. This is called a singleton — only one instance exists no matter how many components use it. All components that inject this service share the same instance and the same data.

Injecting a Service into a Component

To use a service in a component, declare it as a parameter in the component's constructor. Angular's injector sees the type annotation and automatically provides the correct instance.


// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../services/product.service';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent implements OnInit {

  products: any[] = [];
  totalValue = 0;

  constructor(private productService: ProductService) {
    // Angular automatically provides the ProductService instance here
  }

  ngOnInit() {
    this.products = this.productService.getAllProducts();
    this.totalValue = this.productService.getTotalValue();
  }
}

<!-- product-list.component.html -->
<h3>Products (Total Value: ${{ totalValue | number:'1.2-2' }})</h3>
<ul>
  <li *ngFor="let product of products">
    {{ product.name }} — ${{ product.price | currency }}
  </li>
</ul>

The private productService: ProductService in the constructor tells Angular: "this component needs a ProductService instance." Angular creates one (or reuses the existing singleton) and provides it automatically.

Sharing Data Between Components via a Service

One of the most practical uses of services is sharing state between unrelated components. Here is an example of a cart service that multiple components can use:


// cart.service.ts
import { Injectable } from '@angular/core';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

@Injectable({
  providedIn: 'root'
})
export class CartService {

  private items: CartItem[] = [];

  addToCart(item: CartItem) {
    const existing = this.items.find(i => i.id === item.id);
    if (existing) {
      existing.quantity += 1;
    } else {
      this.items.push({ ...item, quantity: 1 });
    }
  }

  removeFromCart(id: number) {
    this.items = this.items.filter(i => i.id !== id);
  }

  getItems(): CartItem[] {
    return this.items;
  }

  getCartCount(): number {
    return this.items.reduce((total, item) => total + item.quantity, 0);
  }

  getTotalPrice(): number {
    return this.items.reduce((total, item) => total + (item.price * item.quantity), 0);
  }

  clearCart() {
    this.items = [];
  }
}

// product-card.component.ts — Adds items to cart
import { CartService } from '../services/cart.service';

export class ProductCardComponent {
  constructor(private cartService: CartService) {}

  addToCart() {
    this.cartService.addToCart({ id: 1, name: 'Laptop Stand', price: 39.99, quantity: 1 });
  }
}

// navbar.component.ts — Displays cart count
import { CartService } from '../services/cart.service';

export class NavbarComponent {
  constructor(private cartService: CartService) {}

  get cartCount(): number {
    return this.cartService.getCartCount();
  }
}

Both ProductCardComponent and NavbarComponent inject the same CartService singleton. When a product is added through ProductCardComponent, the NavbarComponent immediately reflects the updated count because they share the same service instance.

Service Scope and Providers

The scope of a service determines how many instances are created and where they are available.

Application-Wide Singleton (Most Common)


@Injectable({ providedIn: 'root' })

One instance exists for the entire application lifetime. All components and services share it.

Module-Level Service


// Only available within the specific module
@NgModule({
  providers: [ProductService]
})

A separate instance is created for each module that provides the service.

Component-Level Service


// Only available to this component and its children
@Component({
  selector: 'app-example',
  providers: [ProductService]
})

A new instance is created each time this component is created, and destroyed when the component is destroyed.

A Simple Logger Service Example

Here is a practical logger service that demonstrates service reuse across multiple components:


// logger.service.ts
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class LoggerService {
  private logs: string[] = [];

  log(message: string) {
    const timestamp = new Date().toLocaleTimeString();
    this.logs.push(`[${timestamp}] ${message}`);
    console.log(`[${timestamp}] ${message}`);
  }

  getLogs(): string[] {
    return this.logs;
  }

  clearLogs() {
    this.logs = [];
  }
}

// Any component can use it
export class SomeComponent {
  constructor(private logger: LoggerService) {}

  doSomething() {
    this.logger.log('User performed an action');
    // ... perform the action
  }
}

Summary

Services are TypeScript classes marked with @Injectable that contain shared logic, data, or business rules. They keep components lean by moving complex logic out of the component class. Dependency injection is Angular's mechanism for automatically providing service instances to the classes that need them. Setting providedIn: 'root' creates a single application-wide singleton. Services are injected through the constructor using TypeScript's type annotation system. Multiple components can share state through a singleton service, making services ideal for cross-component communication and shared data management.

Leave a Comment

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