Welcome back! Today we’re unlocking one of Angular’s most powerful features: Dependency Injection (DI). This system manages your services – the reusable pieces of logic that power your application’s functionality.
Why Services Matter
Services help you:
- Share data and logic across components
- Encapsulate business logic
- Communicate with backend APIs
- Maintain application state
- Avoid code duplication
Creating Your First Service
Generate a service using Angular CLI:
bash
ng generate service data # Shorthand: ng g s data
This creates:
typescript
// data.service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' // <- This is key! }) export class DataService { constructor() { } }
Understanding @Injectable()
The decorator tells Angular:
- This class can be injected
- How it should be provided (singleton by default with
providedIn: 'root'
)
Basic Service Example
Let’s create a simple counter service:
typescript
@Injectable({ providedIn: 'root' }) export class CounterService { private count = 0; increment() { this.count++; } getCount() { return this.count; } reset() { this.count = 0; } }
Injecting Services into Components
Use constructor injection:
typescript
// app.component.ts @Component({...}) export class AppComponent { constructor(private counterService: CounterService) {} increment() { this.counterService.increment(); console.log(this.counterService.getCount()); } }
Dependency Injection in Action
Angular’s DI system works like this:
- You declare a dependency (via constructor)
- Angular looks for a provider of that service
- It either:
- Creates a new instance, or
- Returns an existing one (for singletons)
Service Providers: Three Ways to Provide
- Root Level (Singleton):typescriptCopyDownload@Injectable({ providedIn: ‘root’ // Recommended default })
- Module Level:typescriptCopyDownload@NgModule({ providers: [DataService] // Available to all in module })
- Component Level:typescriptCopyDownload@Component({ providers: [DataService] // New instance per component })
Real-World Service Example: API Service
Here’s a typical HTTP service:
typescript
@Injectable({ providedIn: 'root' }) export class ApiService { private apiUrl = 'https://api.example.com'; constructor(private http: HttpClient) {} getUsers() { return this.http.get(`${this.apiUrl}/users`); } createUser(user: User) { return this.http.post(`${this.apiUrl}/users`, user); } }
Don’t forget to import HttpClientModule
in your root module!
Advanced DI Techniques
1. Injection Tokens
For non-class dependencies (like config objects):
typescript
// tokens.ts export const API_CONFIG = new InjectionToken('app.config'); // app.module.ts providers: [ { provide: API_CONFIG, useValue: { apiUrl: 'https://api.example.com' } } ] // api.service.ts constructor(@Inject(API_CONFIG) private config: any) {}
2. Factory Providers
Complex initialization logic:
typescript
providers: [ { provide: DataService, useFactory: (http: HttpClient) => { return new DataService(http, environment.apiUrl); }, deps: [HttpClient] } ]
3. Optional Dependencies
typescript
constructor(@Optional() private logger?: LoggerService) { this.logger?.log('Component created'); }
Service Communication Patterns
- Shared Service:typescriptCopyDownload// data.service.ts private dataSubject = new BehaviorSubject<string>(‘default’); data$ = this.dataSubject.asObservable(); updateData(value: string) { this.dataSubject.next(value); }
- Component Interaction Service:typescriptCopyDownload// interaction.service.ts private buttonClicked = new Subject<void>(); buttonClicked$ = this.buttonClicked.asObservable(); notifyButtonClicked() { this.buttonClicked.next(); }
Best Practices
- Single Responsibility: Each service should do one thing well
- State Management: For complex state, consider NgRx
- Lazy Loading: Services in lazy-loaded modules get their own instances
- Testing: Mock services in component tests
Common Pitfalls
❌ Creating overly large “god” services
❌ Forgetting to provide services at the right level
❌ Not unsubscribing from observables
❌ Putting component logic in services
Hands-On Exercise
Create an AuthService
with:
login(username, password)
method (simulate API call)logout()
methodisAuthenticated$
observable- Store token in memory (no localStorage yet)
What’s Next?
In Part 9, we’ll dive into HTTP Client – how to communicate with backend APIs!