Chapter 6: Services & Dependency Injection
Services & Dependency Injection!
This is where your app stops being a bunch of isolated components and starts acting like a real, organized system. Services are the place to put shared logic, state management (especially with signals), API calls, business rules, or anything that shouldn’t live inside a single component.
We’ll explain it like we’re pair-programming in VS Code at your desk in Airoli (it’s 1:45 PM IST, perfect chai time ☕). We’ll use modern Angular 19+ / 2025–2026 style: standalone components, inject() function (preferred over constructor injection), and signals inside services.
1. What is a Service? (Quick teacher explanation)
A service is just a plain TypeScript class decorated with @Injectable(). Its job:
- Hold shared state (e.g., current user, shopping cart, theme)
- Fetch/save data (HTTP, localStorage, IndexedDB)
- Provide reusable functions (formatting dates, validation, calculations)
- Act as a “single source of truth” for certain data across components
Services become powerful when combined with signals — you can have reactive, shared state without RxJS Subjects in most cases.
2. Creating an Injectable Service (Modern way)
Let’s create a real example: a UserService that manages the current logged-in user with signals.
|
0 1 2 3 4 5 6 |
ng generate service user --standalone |
(or manually create src/app/user.service.ts)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { Injectable, signal, computed } from '@angular/core'; @Injectable({ providedIn: 'root' // ← most common & recommended for app-wide singletons }) export class UserService { // Writable signal for the current user (our source of truth) private _currentUser = signal<User | null>(null); // Readonly public API currentUser = this._currentUser.asReadonly(); // Derived / computed signals isLoggedIn = computed(() => !!this.currentUser()); userName = computed(() => this.currentUser()?.name ?? 'Guest'); isAdmin = computed(() => this.currentUser()?.role === 'admin'); // Methods to change state login(name: string, role: 'user' | 'admin' = 'user') { this._currentUser.set({ name, role, lastLogin: new Date() }); } logout() { this._currentUser.set(null); } updateName(newName: string) { this._currentUser.update(user => user ? { ...user, name: newName } : null); } } interface User { name: string; role: 'user' | 'admin'; lastLogin: Date; } |
Notice:
- We keep the writable signal private (_currentUser)
- Expose readonly version + computed for safety & reactivity
- Methods use .set() / .update() → any component reading currentUser() or isLoggedIn() auto-updates
3. providedIn: ‘root’ vs Component providers (Very important distinction!)
This is where most confusion happens — let’s compare clearly.
| Option | Scope / Lifetime | Instance Count | Best For | When to Use in 2026 |
|---|---|---|---|---|
| providedIn: ‘root’ | Application-wide singleton | 1 instance | Global state (user, theme, config, API client) | 90% of services — default recommendation |
| providedIn: ‘any’ | One instance per lazy-loaded module | Multiple | Rare — optimization in large lazy apps | Almost never now |
| providedIn: someModule | One instance per module (legacy) | Multiple | Old NgModule projects | Avoid in standalone |
| providers: [UserService] in component | Instance per component + its children | New per component | Local state per route/component tree (e.g., form wizard state, tab-specific data) | When you want isolation |
| providers: [{ provide: UserService, useClass: MockUserService }] | Override for testing / subtree | Varies | Testing, feature flags, multi-tenant apps | Advanced cases |
Key teacher rule (2025–2026):
- Start with providedIn: ‘root’ → it’s a singleton → shared state works perfectly with signals
- Only move to component-level providers when you intentionally want separate instances (e.g., one shopping cart per tab, different form data per wizard step)
Example: Component-level provider (different instances)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// In a component that needs its OWN user state (rare, but useful) @Component({ selector: 'app-profile-editor', standalone: true, providers: [UserService], // ← NEW instance just for this component + children template: `...` }) export class ProfileEditorComponent { // This UserService is DIFFERENT from the root one } |
→ Two <app-profile-editor> components would each have their own isolated UserService instance.
4. Injecting Services with inject() (The modern preferred way)
Since Angular 14+, and strongly recommended in 2025–2026 style guides:
- Prefer inject() over constructor injection
- Reasons: better readability, easier comments, better type inference, works great with class fields, inheritance-friendly, no constructor boilerplate
Old constructor style (still works, but less recommended now)
|
0 1 2 3 4 5 6 |
constructor(private userService: UserService) {} |
Modern inject() style
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// In any standalone component import { Component, inject } from '@angular/core'; import { UserService } from './user.service'; @Component({...}) export class NavbarComponent { private userService = inject(UserService); // ← inject here! // Or one-liner (very common) userName = inject(UserService).userName; // direct access // Or destructure-like (clean!) currentUser = inject(UserService).currentUser; isLoggedIn = inject(UserService).isLoggedIn; login() { this.userService.login('Webliance', 'admin'); } } |
Template:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
@if (isLoggedIn()) { <p>Welcome, {{ userName() }}! 👋</p> <button (click)="userService.logout()">Logout</button> } @else { <button (click)="login()">Login as Webliance</button> } |
→ Because currentUser() and isLoggedIn() are signals (from service), the template auto-re-renders when you call login() or logout() — pure reactivity!
5. Mini Practice Task (build this now!)
- Use the UserService above (providedIn: ‘root’)
- Create two components:
- app-header.component.ts → shows {{ userName() }} and login/logout button
- app-dashboard.component.ts → shows isAdmin() and some admin-only content with @if
- Inject UserService with inject() in both
- Login from header → see dashboard auto-update (proves singleton + signal reactivity)
Bonus:
- Try temporarily adding providers: [UserService] to one component → see how login in one place doesn’t affect the other (isolated instances)
Quick Rules of Thumb (pin on your wall)
- Most services → @Injectable({ providedIn: ‘root’ }) + signals inside
- Inject with inject() → cleaner, modern default
- Expose readonly + computed → prevent accidental mutation from consumers
- Component providers → only when you want per-instance state
- Never mutate inputs/params directly — use service methods
You’ve now connected components with shared, reactive state — this is where Angular apps become truly powerful and maintainable.
Next chapter is usually Routing Basics (navigation, lazy loading, etc.). Ready to go there, or want to deepen services (e.g., HTTP integration with signals, effect() in services, multi-provider tokens, testing services)? Just tell me — we’re on a roll! 🚀
