Service-Based Communication
Services enable any component to communicate with any other component, regardless of their position in the component tree. No @Input drilling, no @Output chains.
@Input/@Output works great for parent-child. But what about:
- Siblings โ two components under the same parent?
- Deeply nested โ grandchild to grandparent?
- Unrelated โ sidebar and header that aren't parent-child?
"Service = WhatsApp group โ sab members share data, parent-child relation nahi chahiye."
The solution: A shared service injected into both components acts as a communication bridge. One component writes data to the service, the other reads it. No direct connection needed.
// Both components inject the same service
@Component({...}) export class MenuComponent {
private cartService = inject(CartService);
addToCart(item: MenuItem) {
this.cartService.addItem(item); // Write to service
}
}
@Component({...}) export class CartBadgeComponent {
private cartService = inject(CartService);
// Read from service โ in template!
count = this.cartService.itemCount;
}
Components don't need to know about each other. They only need to know about the service.
The traditional approach uses BehaviorSubject in the service. This is still widely used in existing codebases.
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface CartItem {
id: number; name: string; price: number; quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
// Private Subject โ encapsulates write access
private cartItemsSubject = new BehaviorSubject<CartItem[]>([]);
// Public Observable โ read-only for consumers
cartItems$: Observable<CartItem[]> = this.cartItemsSubject.asObservable();
// Derived observable
total$: Observable<number> = this.cartItems$.pipe(
map(items => items.reduce((sum, i) => sum + i.price * i.quantity, 0))
);
count$: Observable<number> = this.cartItems$.pipe(
map(items => items.length)
);
// Public methods to modify state
addItem(item: CartItem) {
this.cartItemsSubject.next([...this.cartItemsSubject.value, item]);
}
removeItem(id: number) {
this.cartItemsSubject.next(
this.cartItemsSubject.value.filter(i => i.id !== id)
);
}
clearCart() {
this.cartItemsSubject.next([]);
}
}
"BehaviorSubject = shared whiteboard โ koi bhi likho, sab padh sakte ho. Lekin subscribe karna padta hai."
Consumer component (old way):
export class CartBadgeComponent implements OnInit, OnDestroy {
private cartService = inject(CartService);
count = 0;
private sub!: Subscription;
ngOnInit() {
this.sub = this.cartService.count$.subscribe(c => this.count = c);
}
ngOnDestroy() {
this.sub?.unsubscribe(); // Don't forget!
}
}The modern approach uses signal() in the service. Same result, simpler code.
import { Injectable, signal, computed } from '@angular/core';
export interface CartItem {
id: number; name: string; price: number; quantity: number;
}
@Injectable({ providedIn: 'root' })
export class CartService {
// Private writable signal
private cartItems = signal<CartItem[]>([]);
// Public readonly signal
readonly items = this.cartItems.asReadonly();
// Computed โ derived values
readonly total = computed(() =>
this.items().reduce((sum, i) => sum + i.price * i.quantity, 0)
);
readonly itemCount = computed(() => this.items().length);
readonly formattedTotal = computed(() => `โน${this.total()}`);
// Public methods
addItem(item: CartItem) {
this.cartItems.update(items => [...items, item]);
}
removeItem(id: number) {
this.cartItems.update(items => items.filter(i => i.id !== id));
}
updateQuantity(id: number, quantity: number) {
this.cartItems.update(items =>
items.map(i => i.id === id ? { ...i, quantity } : i)
);
}
clearCart() {
this.cartItems.set([]);
}
}
"Signal = modern whiteboard โ same result, simpler code, no subscribe."
Consumer component (new way):
export class CartBadgeComponent {
private cartService = inject(CartService);
// Read directly โ no ngOnInit, no ngOnDestroy!
itemCount = this.cartService.itemCount;
total = this.cartService.total;
}
// Template: {{ itemCount() }} โ no async pipe needed!When to use service communication:
๐ก Use Service When...
- Sibling components โ side-by-side components need to share data (cart + badge)
- Deeply nested โ avoid "prop drilling" (@Input through 5+ levels)
- Global state โ auth, notifications, theme, language settings
- Unrelated components โ header, sidebar, router outlet that aren't in direct parent-child
"Prop drilling = parcel 5 logon se pass karna โ service = direct delivery."
Rule of thumb: If data passes through 3+ levels of @Input (GrandParent โ Parent โ Child), switch to a service. Beyond 2 levels, @Input becomes "drilling" and is hard to maintain.
// โ Bad: Prop drilling through 4 levels
<grandparent [user]="user">
<parent [user]="user">
<child [user]="user">
<grandchild [user]="user"> โ Why does parent need user?
{{ user.name }}
</grandchild>
</child>
</parent>
</grandparent>
// โ
Good: Service injected where needed
<grandchild>{{ authService.user()?.name }}</grandchild>Common anti-patterns to avoid:
1. Exposing writable service state directly
// โ WRONG โ any component can corrupt state
cartItems = signal<CartItem[]>([]); // Public AND writable!
// โ
CORRECT โ read-only exposed
private cartItems = signal<CartItem[]>([]);
readonly items = this.cartItems.asReadonly();
2. Using service for simple parent-child data
// โ WRONG โ @Input is simpler for direct parent-child
// Child component using service just to get data from parent
// โ
CORRECT โ @Input for one-level parent-child
@Input() biryaniName = '';
3. Putting component-specific state in service
// โ WRONG โ form data belongs to component
// @Injectable() export class FormService {
// formData = signal({ name: '', email: '' }); // Component-specific!
// }
// โ
CORRECT โ form state in component
export class FormComponent {
formData = signal({ name: '', email: '' }); // Local state
}
4. Creating service for one-time parent-child
// โ WRONG โ over-engineering
// @Injectable() export class OneTimeDataService { ... }
// Use @Input instead for one-level communication
// โ
Use @Input for direct, @Output for events, service for multi-level
"Service = public place โ sirf shared data rakho, personal data nahi."
Key Takeaways
- โ Services enable any component to communicate โ no parent-child needed
- โ Use for: siblings, deeply nested, global state, unrelated components
- โ Signal pattern: signal() + asReadonly() + computed() โ no subscribe
- โ Always expose asReadonly() or asObservable() โ don't leak write access
- โ Don't use service for simple parent-child โ @Input is simpler
- โ Don't put component-specific state in shared services
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login