Services & Dependency Injection ๐ง
Services are classes that handle business logic, data access, and shared functionality. Dependency Injection (DI) is Angularโs way of providing services to components automatically.
๐ฏ What are Services?
Section titled โ๐ฏ What are Services?โServices are singleton classes that:
- Handle business logic
- Manage data and state
- Communicate with APIs
- Share functionality between components
- Keep components focused on presentation
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root'})export class UserService { private users = [ { id: 1, name: 'John Doe', email: 'john@example.com' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com' } ];
getUsers() { return this.users; }
getUserById(id: number) { return this.users.find(user => user.id === id); }
addUser(user: any) { const newUser = { ...user, id: Date.now() }; this.users.push(newUser); return newUser; }}๐๏ธ Creating Services
Section titled โ๐๏ธ Creating ServicesโUsing Angular CLI (Recommended)
Section titled โUsing Angular CLI (Recommended)โ# Generate a new serviceng generate service userng generate service services/data
# Short formng g s userng g s services/dataManual Creation
Section titled โManual Creationโimport { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' // Available app-wide})export class UserService { // Service logic here}๐ง Dependency Injection Basics
Section titled โ๐ง Dependency Injection BasicsโInjecting Services into Components
Section titled โInjecting Services into Componentsโimport { Component, inject } from '@angular/core';import { UserService } from './user.service';
@Component({ selector: 'app-user-list', template: ` <div> @for (user of users; track user.id) { <div class="user-card"> <h3>{{user.name}}</h3> <p>{{user.email}}</p> </div> } </div> `})export class UserList { // Modern inject() function (Angular 14+) private userService = inject(UserService);
users = this.userService.getUsers();
// Alternative: Constructor injection (still supported) // constructor(private userService: UserService) { // this.users = this.userService.getUsers(); // }}๐ Service Patterns
Section titled โ๐ Service Patternsโ1. Data Service
Section titled โ1. Data Serviceโ@Injectable({ providedIn: 'root'})export class ProductService { private products = [ { id: 1, name: 'Laptop', price: 999, category: 'Electronics' }, { id: 2, name: 'Book', price: 29, category: 'Education' } ];
getAllProducts() { return [...this.products]; // Return copy to prevent mutation }
getProductsByCategory(category: string) { return this.products.filter(p => p.category === category); }
addProduct(product: Omit<Product, 'id'>) { const newProduct = { ...product, id: this.generateId() }; this.products.push(newProduct); return newProduct; }
updateProduct(id: number, updates: Partial<Product>) { const index = this.products.findIndex(p => p.id === id); if (index !== -1) { this.products[index] = { ...this.products[index], ...updates }; return this.products[index]; } return null; }
deleteProduct(id: number) { const index = this.products.findIndex(p => p.id === id); if (index !== -1) { return this.products.splice(index, 1)[0]; } return null; }
private generateId(): number { return Math.max(...this.products.map(p => p.id), 0) + 1; }}
interface Product { id: number; name: string; price: number; category: string;}2. State Management Service (Angular 16+ with Signals)
Section titled โ2. State Management Service (Angular 16+ with Signals)โNote: This pattern is recommended for Angular 16+ with Signals. Will take more about signals in the next modern angular section.
import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root'})export class CartService { // Using signals for reactive state (Angular 16+) private cartItems = signal<CartItem[]>([]);
// Computed values items = this.cartItems.asReadonly(); totalItems = computed(() => this.cartItems().reduce((sum, item) => sum + item.quantity, 0) ); totalPrice = computed(() => this.cartItems().reduce((sum, item) => sum + (item.price * item.quantity), 0) );
addToCart(product: Product, quantity = 1) { const existingItem = this.cartItems().find(item => item.id === product.id);
if (existingItem) { this.updateQuantity(product.id, existingItem.quantity + quantity); } else { this.cartItems.update(items => [...items, { id: product.id, name: product.name, price: product.price, quantity }]); } }
removeFromCart(productId: number) { this.cartItems.update(items => items.filter(item => item.id !== productId)); }
updateQuantity(productId: number, quantity: number) { if (quantity <= 0) { this.removeFromCart(productId); return; }
this.cartItems.update(items => items.map(item => item.id === productId ? { ...item, quantity } : item ) ); }
clearCart() { this.cartItems.set([]); }}
interface CartItem { id: number; name: string; price: number; quantity: number;}3. Utility Service
Section titled โ3. Utility Serviceโ@Injectable({ providedIn: 'root'})export class UtilityService { formatCurrency(amount: number, currency = 'USD'): string { return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount); }
formatDate(date: Date | string, format = 'short'): string { const dateObj = typeof date === 'string' ? new Date(date) : date; return new Intl.DateTimeFormat('en-US', { dateStyle: format as any }).format(dateObj); }
generateId(): string { return Math.random().toString(36).substring(2) + Date.now().toString(36); }
debounce<T extends (...args: any[]) => any>( func: T, delay: number ): (...args: Parameters<T>) => void { let timeoutId: ReturnType<typeof setTimeout>; return (...args: Parameters<T>) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }
deepClone<T>(obj: T): T { return JSON.parse(JSON.stringify(obj)); }}๐ฏ Provider Scopes
Section titled โ๐ฏ Provider Scopesโ1. Root Level (App-wide Singleton)
Section titled โ1. Root Level (App-wide Singleton)โ@Injectable({ providedIn: 'root' // Single instance for entire app})export class GlobalService { }2. Component Level
Section titled โ2. Component Levelโ@Component({ selector: 'app-feature', providers: [FeatureService], // New instance per component template: `...`})export class FeatureComponent { constructor(private featureService: FeatureService) {}}3. Module Level (Legacy)
Section titled โ3. Module Level (Legacy)โ@NgModule({ providers: [ModuleService] // Single instance per module})export class FeatureModule { }๐ Service Communication
Section titled โ๐ Service Communicationโ1. Parent-Child via Service
Section titled โ1. Parent-Child via Serviceโ@Injectable({ providedIn: 'root'})export class MessageService { private messageSubject = new BehaviorSubject<string>(''); message$ = this.messageSubject.asObservable();
sendMessage(message: string) { this.messageSubject.next(message); }
clearMessage() { this.messageSubject.next(''); }}
// Component Aexport class ComponentA { private messageService = inject(MessageService);
sendMessage() { this.messageService.sendMessage('Hello from Component A!'); }}
// Component Bexport class ComponentB { private messageService = inject(MessageService); message$ = this.messageService.message$;}2. Event Bus Pattern
Section titled โ2. Event Bus Patternโ@Injectable({ providedIn: 'root'})export class EventBusService { private eventSubject = new Subject<AppEvent>(); events$ = this.eventSubject.asObservable();
emit(event: AppEvent) { this.eventSubject.next(event); }
on(eventType: string) { return this.events$.pipe( filter(event => event.type === eventType) ); }}
interface AppEvent { type: string; data?: any;}
// Usageexport class SomeComponent { private eventBus = inject(EventBusService);
ngOnInit() { this.eventBus.on('user-login').subscribe(event => { console.log('User logged in:', event.data); }); }
login() { this.eventBus.emit({ type: 'user-login', data: { userId: 123, name: 'John' } }); }}๐จ Practical Example: Todo Service
Section titled โ๐จ Practical Example: Todo Serviceโimport { Injectable, signal, computed } from '@angular/core';
export interface Todo { id: number; text: string; completed: boolean; createdAt: Date;}
@Injectable({ providedIn: 'root'})export class TodoService { private todos = signal<Todo[]>([ { id: 1, text: 'Learn Angular Services', completed: false, createdAt: new Date() }, { id: 2, text: 'Build Todo App', completed: false, createdAt: new Date() } ]);
// Computed properties allTodos = this.todos.asReadonly(); activeTodos = computed(() => this.todos().filter(todo => !todo.completed)); completedTodos = computed(() => this.todos().filter(todo => todo.completed)); totalCount = computed(() => this.todos().length); activeCount = computed(() => this.activeTodos().length); completedCount = computed(() => this.completedTodos().length);
addTodo(text: string) { if (!text.trim()) return;
const newTodo: Todo = { id: this.generateId(), text: text.trim(), completed: false, createdAt: new Date() };
this.todos.update(todos => [...todos, newTodo]); }
toggleTodo(id: number) { this.todos.update(todos => todos.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }
updateTodo(id: number, text: string) { if (!text.trim()) return;
this.todos.update(todos => todos.map(todo => todo.id === id ? { ...todo, text: text.trim() } : todo ) ); }
deleteTodo(id: number) { this.todos.update(todos => todos.filter(todo => todo.id !== id)); }
clearCompleted() { this.todos.update(todos => todos.filter(todo => !todo.completed)); }
toggleAll() { const allCompleted = this.todos().every(todo => todo.completed); this.todos.update(todos => todos.map(todo => ({ ...todo, completed: !allCompleted })) ); }
private generateId(): number { const ids = this.todos().map(todo => todo.id); return ids.length > 0 ? Math.max(...ids) + 1 : 1; }}Using the Todo Service
Section titled โUsing the Todo Serviceโimport { Component, inject } from '@angular/core';import { TodoService } from './todo.service';import { FormsModule } from '@angular/forms';import { CommonModule } from '@angular/common';
@Component({ selector: 'app-todo-list', imports: [CommonModule, FormsModule], template: ` <div class="todo-app"> <h1>Todo App</h1>
<!-- Add todo --> <div class="add-todo"> <input [(ngModel)]="newTodoText" (keyup.enter)="addTodo()" placeholder="What needs to be done?"> <button (click)="addTodo()">Add</button> </div>
<!-- Stats --> <div class="stats"> <span>Total: {{todoService.totalCount()}}</span> <span>Active: {{todoService.activeCount()}}</span> <span>Completed: {{todoService.completedCount()}}</span> </div>
<!-- Todo list --> <div class="todo-list"> @for (todo of filteredTodos(); track todo.id) { <div class="todo-item" [class.completed]="todo.completed"> <input type="checkbox" [checked]="todo.completed" (change)="todoService.toggleTodo(todo.id)"> <span class="todo-text">{{todo.text}}</span> <button (click)="todoService.deleteTodo(todo.id)">Delete</button> </div> } </div>
<!-- Actions --> <div class="actions"> <button (click)="todoService.toggleAll()">Toggle All</button> <button (click)="todoService.clearCompleted()">Clear Completed</button> </div> </div> `})export class TodoList { todoService = inject(TodoService); newTodoText = ''; filter: 'all' | 'active' | 'completed' = 'all';
filteredTodos = computed(() => { switch (this.filter) { case 'active': return this.todoService.activeTodos(); case 'completed': return this.todoService.completedTodos(); default: return this.todoService.allTodos(); } });
addTodo() { if (this.newTodoText.trim()) { this.todoService.addTodo(this.newTodoText); this.newTodoText = ''; } }}โ Best Practices
Section titled โโ Best Practicesโ1. Single Responsibility
Section titled โ1. Single ResponsibilityโEach service should have one clear purpose.
2. Use inject() Function
Section titled โ2. Use inject() Functionโ// โ
Modern approachprivate userService = inject(UserService);
// โ Legacy (still works)constructor(private userService: UserService) {}3. Provide at Root Level
Section titled โ3. Provide at Root Levelโ@Injectable({ providedIn: 'root' // Preferred for most services})4. Use Signals for State
Section titled โ4. Use Signals for Stateโ// โ
Reactive with signalsprivate state = signal(initialState);
// โ Manual change detectionprivate state = initialState;5. Return Copies, Not References
Section titled โ5. Return Copies, Not Referencesโ// โ
Prevent external mutationgetUsers() { return [...this.users];}
// โ Exposes internal stategetUsers() { return this.users;}6. Use TypeScript Interfaces
Section titled โ6. Use TypeScript Interfacesโinterface User { id: number; name: string; email: string;}๐ฏ Quick Checklist
Section titled โ๐ฏ Quick Checklistโ- Understand what services are and their purpose
- Know how to create services with
@Injectable() - Use
inject()function for modern DI - Implement different service patterns (data, state, utility)
- Understand provider scopes (root, component, module)
- Use signals for reactive state management
- Follow single responsibility principle
- Return copies to prevent mutation
- Use TypeScript for type safety
๐ Next Steps
Section titled โ๐ Next Stepsโ- Routing Basics - Navigate between components
- Forms Introduction - Handle user input
- HTTP Client - Communicate with APIs
Remember: Services are the backbone of Angular applications. They keep your components clean and enable powerful patterns like dependency injection and reactive programming! ๐ง